ciphermesh 2.12.0 → 2.14.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 +187 -0
- package/README.md +8 -6
- package/README.pt-BR.md +8 -6
- package/docs/ARCHITECTURE.md +332 -230
- package/docs/PROTOCOL.md +160 -5
- package/docs/SETUP.md +18 -0
- package/docs/commands.json +15 -7
- package/docs/design/multi-device.md +290 -0
- package/docs/design/sender-keys-on-relay.md +34 -3
- package/package.json +3 -3
- package/src/client/ChatController.js +736 -26
- package/src/client/UI.js +617 -124
- package/src/client/keyboard.js +388 -0
- package/src/crypto/DeviceIdentity.js +307 -0
- package/src/crypto/KeyManager.js +176 -4
- package/src/crypto/TrustStore.js +153 -0
- package/src/p2p/P2PChatController.js +94 -4
- package/src/protocol/messages.js +25 -1
- package/src/protocol/validators.js +16 -0
- package/src/server/SessionManager.js +63 -6
- package/src/server/WebSocketServer.js +40 -1
- package/src/shared/constants.js +9 -1
- package/src/shared/desktopNotify.js +204 -0
- package/src/shared/deviceProvisioning.js +112 -0
- package/src/shared/notifyWorker.js +46 -0
- package/src/shared/tips.js +1 -0
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# CipherMesh — Complete Technical Documentation
|
|
2
2
|
|
|
3
3
|
> Secure chat for a local area network (LAN) with real end-to-end encryption (E2EE).
|
|
4
4
|
> The server **never** has access to the content of messages.
|
|
@@ -26,18 +26,18 @@
|
|
|
26
26
|
|
|
27
27
|
### What it is
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
CipherMesh is an instant messaging system designed to operate **exclusively within a local area network (LAN)**. It uses end-to-end encryption (E2EE) based on **Curve25519 + XSalsa20-Poly1305** (via libsodium), ensuring that the server acts only as a **blind relay** — it forwards bytes it cannot read.
|
|
30
30
|
|
|
31
31
|
### Core Principle
|
|
32
32
|
|
|
33
33
|
```
|
|
34
|
-
|
|
34
|
+
Client A Server Client B
|
|
35
35
|
| | |
|
|
36
|
-
|--- payload
|
|
37
|
-
| |--- payload
|
|
36
|
+
|--- encrypted payload --->| |
|
|
37
|
+
| |--- encrypted payload --->|
|
|
38
38
|
| | |
|
|
39
|
-
|
|
|
40
|
-
|
|
|
39
|
+
| The server does NOT | |
|
|
40
|
+
| hold the key to open it | |
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
The server knows **only**:
|
|
@@ -55,15 +55,15 @@ The server **never** knows:
|
|
|
55
55
|
|
|
56
56
|
```
|
|
57
57
|
┌──────────────┐
|
|
58
|
-
│
|
|
59
|
-
│
|
|
58
|
+
│ Server │
|
|
59
|
+
│ (Relay) │
|
|
60
60
|
│ :3600 │
|
|
61
61
|
└──────┬───────┘
|
|
62
62
|
│ WebSocket
|
|
63
63
|
┌────────────┼────────────┐
|
|
64
64
|
│ │ │
|
|
65
65
|
┌─────┴─────┐ ┌───┴───┐ ┌─────┴─────┐
|
|
66
|
-
│
|
|
66
|
+
│ Client A │ │ ... │ │ Client N │
|
|
67
67
|
│ (Terminal) │ │ │ │ (Terminal) │
|
|
68
68
|
└───────────┘ └───────┘ └───────────┘
|
|
69
69
|
```
|
|
@@ -74,14 +74,14 @@ This is the **star** topology (star topology) — the default mode. All clients
|
|
|
74
74
|
|
|
75
75
|
```
|
|
76
76
|
┌───────────┐
|
|
77
|
-
│
|
|
77
|
+
│ Client A │
|
|
78
78
|
│ (Terminal) │
|
|
79
79
|
└─────┬─────┘
|
|
80
80
|
│ WebSocket direto
|
|
81
81
|
┌─────────┼──────────┐
|
|
82
82
|
│ │
|
|
83
83
|
┌───┴───┐ ┌────┴────┐
|
|
84
|
-
│ ... │ │
|
|
84
|
+
│ ... │ │ Client N │
|
|
85
85
|
│ │ │(Terminal)│
|
|
86
86
|
└───────┘ └─────────┘
|
|
87
87
|
```
|
|
@@ -94,13 +94,24 @@ In P2P mode (`npm run p2p`), peers discover each other via mDNS on the LAN and c
|
|
|
94
94
|
|
|
95
95
|
### Production Dependencies
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
|
103
|
-
|
|
|
97
|
+
<!-- The package list is checked against package.json by
|
|
98
|
+
test/architecture-doc.test.js. Versions deliberately live only in
|
|
99
|
+
package.json: a number copied here is a number that goes stale, and
|
|
100
|
+
pinning it would put a doc edit in the way of every dependency bump. -->
|
|
101
|
+
|
|
102
|
+
| Package | Role |
|
|
103
|
+
| --- | --- |
|
|
104
|
+
| **@noble/post-quantum** | ML-KEM-768 for the post-quantum hybrid handshake (`src/crypto/PQHybrid.js`). libsodium has no ML-KEM, and this one is audited. |
|
|
105
|
+
| **blessed** | Terminal UI — boxes, scrolling, colours, borders. ncurses-shaped, in JS. Its key parser is old enough to need a shim (`src/client/keyboard.js`). |
|
|
106
|
+
| **bonjour-service** | mDNS service discovery, so P2P peers find each other on the LAN with no relay (`src/p2p/Discovery.js`). |
|
|
107
|
+
| **boxen** | The framed server and P2P banners (`src/shared/banner.js`, `src/p2p/index.js`). |
|
|
108
|
+
| **chalk** | Coloured output outside the blessed UI — banners, prompts, the boot sequence. ESM-only in v5, which suits `"type": "module"`. |
|
|
109
|
+
| **gradient-string** | The gradient across the startup banner (`src/shared/banner.js`). |
|
|
110
|
+
| **jimp** | Decodes received images so they can be drawn as half-blocks in the chat (`src/client/ImagePreview.js`). |
|
|
111
|
+
| **node-notifier** | Desktop notifications. Driven only from `src/shared/notifyWorker.js` — see §4.2 for why it is kept at arm’s length. |
|
|
112
|
+
| **qrcode-terminal** | Renders `/invite` as a QR code in the terminal. |
|
|
113
|
+
| **sodium-native** | Native binding of **libsodium**. Runs in compiled C, not pure JS, so the security is audited and the secret memory is real — see the comparison below. |
|
|
114
|
+
| **ws** | The most mature and performant WebSocket implementation for Node.js. Zero dependencies. Natively supports binary frames (essential for encrypted payloads). |
|
|
104
115
|
|
|
105
116
|
### Why sodium-native and not tweetnacl?
|
|
106
117
|
|
|
@@ -121,11 +132,12 @@ In P2P mode (`npm run p2p`), peers discover each other via mDNS on the LAN and c
|
|
|
121
132
|
### Development Dependencies
|
|
122
133
|
|
|
123
134
|
| Package | Role |
|
|
124
|
-
|
|
125
|
-
|
|
|
126
|
-
|
|
|
127
|
-
| **
|
|
128
|
-
| **
|
|
135
|
+
| --- | --- |
|
|
136
|
+
| **@eslint/js** | ESLint's recommended base configuration. |
|
|
137
|
+
| **eslint** | Linting. |
|
|
138
|
+
| **figlet** | Not used at runtime: `test/banner.test.js` pins the committed ASCII banner against figlet’s own output, so the art cannot drift. |
|
|
139
|
+
| **globals** | Globals definitions (node) for ESLint. |
|
|
140
|
+
| **prettier** | Automatic formatting — consistent code with no style debates. |
|
|
129
141
|
|
|
130
142
|
### Node.js >= 20
|
|
131
143
|
|
|
@@ -140,71 +152,112 @@ Minimum requirement: Node.js 20 LTS. Reasons:
|
|
|
140
152
|
|
|
141
153
|
## 3. Directory Structure
|
|
142
154
|
|
|
155
|
+
<!-- Checked by test/architecture-doc.test.js: every module under src/ has to
|
|
156
|
+
appear here, and nothing here may name a file that does not exist. -->
|
|
157
|
+
|
|
143
158
|
```
|
|
144
|
-
|
|
159
|
+
ciphermesh/
|
|
160
|
+
│
|
|
161
|
+
├── bin/
|
|
162
|
+
│ └── ciphermesh.js # CLI entry point (npx ciphermesh)
|
|
145
163
|
│
|
|
146
164
|
├── docs/
|
|
147
|
-
│
|
|
165
|
+
│ ├── ARCHITECTURE.md
|
|
166
|
+
│ ├── PLUGINS.md
|
|
167
|
+
│ ├── PROTOCOL.md
|
|
168
|
+
│ ├── SETUP.md
|
|
169
|
+
│ └── commands.json
|
|
148
170
|
│
|
|
149
171
|
├── src/
|
|
150
172
|
│ ├── server/
|
|
151
|
-
│ │ ├── index.js
|
|
152
|
-
│ │ ├──
|
|
153
|
-
│ │ ├──
|
|
154
|
-
│ │ ├──
|
|
155
|
-
│ │ ├──
|
|
156
|
-
│ │
|
|
173
|
+
│ │ ├── index.js # Server entry point
|
|
174
|
+
│ │ ├── CertManager.js # TLS certificate generation and loading
|
|
175
|
+
│ │ ├── config.js # Server configuration and env vars
|
|
176
|
+
│ │ ├── ConnectionGuard.js # Rate limits and connection abuse guards
|
|
177
|
+
│ │ ├── MessageRouter.js # Routes encrypted payloads between clients
|
|
178
|
+
│ │ ├── OfflineQueue.js # Queue for messages to offline peers
|
|
179
|
+
│ │ ├── preflight.js # Startup checks before the port is opened
|
|
180
|
+
│ │ ├── presence.js # Presence/hub counters
|
|
181
|
+
│ │ ├── SessionManager.js # Active sessions (connected clients)
|
|
182
|
+
│ │ └── WebSocketServer.js # WebSocket connection handling
|
|
157
183
|
│ │
|
|
158
184
|
│ ├── client/
|
|
159
|
-
│ │ ├── index.js
|
|
160
|
-
│ │ ├──
|
|
161
|
-
│ │ ├── Connection.js
|
|
162
|
-
│ │ ├──
|
|
163
|
-
│ │
|
|
185
|
+
│ │ ├── index.js # Client entry point
|
|
186
|
+
│ │ ├── ChatController.js # Core logic: UI + Connection + Crypto
|
|
187
|
+
│ │ ├── Connection.js # WebSocket connection to the server
|
|
188
|
+
│ │ ├── FileTransfer.js # Encrypted file send/receive (chunks)
|
|
189
|
+
│ │ ├── ImagePreview.js # Half-block image previews
|
|
190
|
+
│ │ ├── keyboard.js # Keyboard-protocol shim (Shift+Enter)
|
|
191
|
+
│ │ └── UI.js # Blessed interface (layout, wrapping, rendering)
|
|
164
192
|
│ │
|
|
165
193
|
│ ├── crypto/
|
|
166
|
-
│ │ ├──
|
|
167
|
-
│ │ ├──
|
|
168
|
-
│ │ ├──
|
|
169
|
-
│ │ ├──
|
|
170
|
-
│ │ ├──
|
|
171
|
-
│ │ ├──
|
|
172
|
-
│ │
|
|
194
|
+
│ │ ├── CertPinStore.js # TLS certificate pinning
|
|
195
|
+
│ │ ├── DeniableEncrypt.js # Deniable (symmetric) message mode
|
|
196
|
+
│ │ ├── DeviceIdentity.js # Ed25519 identity and the signed device list
|
|
197
|
+
│ │ ├── DoubleRatchet.js # PFS via Double Ratchet (DH ratchet + KDF chains)
|
|
198
|
+
│ │ ├── Handshake.js # Public-key exchange protocol
|
|
199
|
+
│ │ ├── HistoryStore.js # Encrypted local history on disk
|
|
200
|
+
│ │ ├── IdentityBackup.js # Encrypted identity + trust export/import
|
|
201
|
+
│ │ ├── KeyManager.js # Key pairs (in memory)
|
|
202
|
+
│ │ ├── MessageCrypto.js # Encrypt/decrypt (crypto_box_easy)
|
|
203
|
+
│ │ ├── NonceManager.js # Nonce generation and replay checks
|
|
204
|
+
│ │ ├── PQHybrid.js # Post-quantum hybrid (X25519 + ML-KEM-768)
|
|
205
|
+
│ │ ├── RoomKey.js # Room key derivation and rotation
|
|
206
|
+
│ │ ├── SealedSender.js # Sealed sender — the relay cannot see who sent what
|
|
207
|
+
│ │ ├── SenderKey.js # Sender keys for group messages
|
|
208
|
+
│ │ ├── StateManager.js # Encrypted state persistence (Argon2id + secretbox)
|
|
209
|
+
│ │ └── TrustStore.js # TOFU + SAS (fingerprint persistence)
|
|
173
210
|
│ │
|
|
174
211
|
│ ├── p2p/
|
|
175
|
-
│ │ ├── index.js
|
|
176
|
-
│ │ ├── Discovery.js
|
|
177
|
-
│ │ ├──
|
|
178
|
-
│ │ ├── PeerConnectionManager.js #
|
|
179
|
-
│ │ └──
|
|
212
|
+
│ │ ├── index.js # P2P mode entry point
|
|
213
|
+
│ │ ├── Discovery.js # mDNS discovery via bonjour-service
|
|
214
|
+
│ │ ├── P2PChatController.js # P2P orchestrator (crypto + UI + peers)
|
|
215
|
+
│ │ ├── PeerConnectionManager.js # Outbound/inbound connection management
|
|
216
|
+
│ │ └── PeerServer.js # Local WebSocket server (random port)
|
|
180
217
|
│ │
|
|
181
218
|
│ ├── protocol/
|
|
182
|
-
│ │ ├──
|
|
183
|
-
│ │
|
|
219
|
+
│ │ ├── capabilities.js # Feature negotiation between versions
|
|
220
|
+
│ │ ├── messages.js # Protocol message types
|
|
221
|
+
│ │ └── validators.js # Payload structure validation
|
|
184
222
|
│ │
|
|
185
223
|
│ └── shared/
|
|
186
|
-
│ ├──
|
|
187
|
-
│
|
|
188
|
-
│
|
|
189
|
-
├──
|
|
190
|
-
│
|
|
191
|
-
│
|
|
192
|
-
│
|
|
193
|
-
│
|
|
194
|
-
│
|
|
195
|
-
│
|
|
196
|
-
│
|
|
197
|
-
│
|
|
224
|
+
│ ├── AuditLog.js # Local audit trail
|
|
225
|
+
│ ├── banner.js # Startup banners
|
|
226
|
+
│ ├── commandSuggest.js # Did-you-mean for mistyped commands
|
|
227
|
+
│ ├── config.js # Client config file
|
|
228
|
+
│ ├── constants.js # Global constants (ports, limits, version)
|
|
229
|
+
│ ├── coverTraffic.js # Cover traffic (anti-metadata)
|
|
230
|
+
│ ├── desktopNotify.js # Desktop notifications — breaker, throttle, isolation
|
|
231
|
+
│ ├── deviceProvisioning.js # Multi-device request/grant/accept
|
|
232
|
+
│ ├── dnd.js # Do-not-disturb / mentions-only gating
|
|
233
|
+
│ ├── doctor.js # Connection diagnosis for /doctor
|
|
234
|
+
│ ├── emoji.js # `:shortcode:` map
|
|
235
|
+
│ ├── fuzzy.js # Fuzzy matching for the palette and pickers
|
|
236
|
+
│ ├── invite.js # Invite strings and QR payloads
|
|
237
|
+
│ ├── keyArt.js # Fingerprint art
|
|
238
|
+
│ ├── lastSession.js # Last-session hints
|
|
239
|
+
│ ├── logger.js # Structured logger (levels and timestamps)
|
|
240
|
+
│ ├── notifyWorker.js # One-shot notification helper, spawned console-less
|
|
241
|
+
│ ├── onboarding.js # First-run setup wizard
|
|
242
|
+
│ ├── panic.js # Duress wipe
|
|
243
|
+
│ ├── pluginCommand.js # The /plugins command
|
|
244
|
+
│ ├── PluginManager.js # Plugin loading and sandboxing
|
|
245
|
+
│ ├── prompt.js # Readline prompts
|
|
246
|
+
│ ├── terminalGraphics.js # kitty/iTerm2 inline image protocols
|
|
247
|
+
│ ├── themes.js # Nick colour themes
|
|
248
|
+
│ ├── tips.js # Security/UX tips
|
|
249
|
+
│ ├── trust.js # Trust badges
|
|
250
|
+
│ └── voiceNote.js # Voice note record/playback
|
|
198
251
|
│
|
|
252
|
+
├── test/ # 90 suites, run with `npm test`
|
|
199
253
|
├── scripts/
|
|
200
|
-
│
|
|
254
|
+
│ ├── generate-commands.mjs # Regenerates docs/commands.json from the code
|
|
255
|
+
│ └── ...
|
|
201
256
|
│
|
|
202
|
-
├── .
|
|
203
|
-
├── .
|
|
204
|
-
├── .gitignore
|
|
205
|
-
├── .npmrc
|
|
257
|
+
├── Formula/ciphermesh.rb # Homebrew formula
|
|
258
|
+
├── eslint.config.js
|
|
206
259
|
├── .prettierrc
|
|
207
|
-
├──
|
|
260
|
+
├── CHANGELOG.md
|
|
208
261
|
├── package.json
|
|
209
262
|
└── README.md
|
|
210
263
|
```
|
|
@@ -263,33 +316,67 @@ securelan-chat/
|
|
|
263
316
|
- Connects to the server and starts the UI
|
|
264
317
|
|
|
265
318
|
#### `src/client/UI.js` — Blessed Interface
|
|
266
|
-
- Layout divided into
|
|
319
|
+
- Layout divided into 4 areas:
|
|
267
320
|
|
|
268
321
|
```
|
|
269
|
-
|
|
270
|
-
│
|
|
271
|
-
|
|
272
|
-
│
|
|
273
|
-
│
|
|
274
|
-
│
|
|
275
|
-
│
|
|
276
|
-
│
|
|
277
|
-
│
|
|
278
|
-
|
|
279
|
-
│
|
|
280
|
-
|
|
322
|
+
┌──────────────────────────────────────────────────────┐
|
|
323
|
+
│ ● CipherMesh ▏ felipe ● 3 online ▏ E2E │ <- Header
|
|
324
|
+
├──────────────────────────────────────────────────────┤
|
|
325
|
+
│ 10:30 🦊 ana │ <- Chat area
|
|
326
|
+
│ Hi! This message wraps well short of the │ (scrollable)
|
|
327
|
+
│ window, not at the border │
|
|
328
|
+
│ │
|
|
329
|
+
│ 10:31 🐧 felipe ✓✓ │
|
|
330
|
+
│ ▎ Oi ana │
|
|
331
|
+
│ │
|
|
332
|
+
│ 10:32 * bob entrou no chat │
|
|
333
|
+
├──────────────────────────────────────────────────────┤
|
|
334
|
+
│ #general Tab ~ Ctrl+K commands ~ /help ~ ^C │ <- Status bar
|
|
335
|
+
├──────────────────────────────────────────────────────┤
|
|
336
|
+
│ > Type your message... │ <- Input box
|
|
337
|
+
└──────────────────────────────────────────────────────┘
|
|
281
338
|
```
|
|
282
339
|
|
|
283
|
-
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
340
|
+
- **Messages are blocks, not lines.** A header naming the sender, then the text
|
|
341
|
+
wrapped at 65 % of the window (78 columns at most) and indented under it. Each
|
|
342
|
+
entry is one `'\n'`-joined string so it stays a single addressable log line —
|
|
343
|
+
reactions, read receipts, edits and the ephemeral burn all still address it by
|
|
344
|
+
index.
|
|
345
|
+
- What distinguishes a message is a coloured rule down the left of the body,
|
|
346
|
+
not its alignment: yellow when it mentions you, magenta for a DM, the accent
|
|
347
|
+
for your own, nothing for a plain incoming one.
|
|
348
|
+
- Runs from one sender fold under a single header, but only inside the same
|
|
349
|
+
minute, so folding never costs the reader a timestamp.
|
|
350
|
+
- `wrapTagged` wraps text that already carries blessed tags: it closes the open
|
|
351
|
+
tag stack at each break and reopens it after, because blessed carries its
|
|
352
|
+
attribute state across the whole content and a tag left open would bleed into
|
|
353
|
+
the next line's gutter. Wrapping before the markdown pass would be simpler and
|
|
354
|
+
would split `**bold**` spans in half.
|
|
355
|
+
- Everything is laid out again on resize, active buffer and stored ones alike,
|
|
356
|
+
from a per-entry recipe kept alongside the rendered string. Entries with no
|
|
357
|
+
recipe — image previews, animation frames — keep exactly what they were given.
|
|
358
|
+
- The status bar shows the room, buffer tabs, and the key shortcuts
|
|
359
|
+
- Chat area with automatic and manual scroll, plus a "new messages ↓" pill
|
|
360
|
+
- Distinct colours per user, with an emoji avatar derived from the nickname
|
|
288
361
|
- Animated "typing..." indicator with support for multiple peers
|
|
289
362
|
- Sound notifications (toggle via `/sound on|off`)
|
|
290
363
|
- Progress bar for file transfers
|
|
291
364
|
- Notifications for users joining/leaving
|
|
292
365
|
|
|
366
|
+
#### `src/client/keyboard.js` — Keyboard protocols
|
|
367
|
+
- A terminal cannot tell Shift+Enter from Enter unless the application asks it
|
|
368
|
+
to, so startup requests the kitty keyboard protocol (`CSI > 1 u`) and xterm's
|
|
369
|
+
`modifyOtherKeys` level 1 (`CSI > 4 ; 1 m`), and undoes both on the way out.
|
|
370
|
+
- blessed's key parser cannot read what comes back — neither a `u` final byte
|
|
371
|
+
after two parameters nor a `~` after three — and would emit `13;2u` as five
|
|
372
|
+
typed characters. So the reports are decoded on the raw byte stream ahead of
|
|
373
|
+
it, installed as `blessed.screen({ input })`.
|
|
374
|
+
- Enter with any modifier becomes a newline; every other enhanced report is
|
|
375
|
+
rewritten to the legacy encoding blessed already understands; anything with no
|
|
376
|
+
legacy equivalent is dropped rather than typed; arrows, function keys, mouse
|
|
377
|
+
reports and bracketed pastes pass through untouched.
|
|
378
|
+
- `CIPHERMESH_LEGACY_KEYS=1` skips the shim and the negotiation entirely.
|
|
379
|
+
|
|
293
380
|
#### `src/client/Connection.js` — WebSocket Client
|
|
294
381
|
- Connects to the server via `ws`
|
|
295
382
|
- Automatic reconnect with exponential backoff (1s, 2s, 4s, 8s, max 30s)
|
|
@@ -306,6 +393,12 @@ securelan-chat/
|
|
|
306
393
|
- Validates fingerprints
|
|
307
394
|
- Manages file transfers via FileTransfer
|
|
308
395
|
- Sound notification when text messages are received
|
|
396
|
+
- Desktop notifications through `src/shared/desktopNotify.js`: rate-limited to
|
|
397
|
+
one per 3 s, muted for the session on the first refusal with one line in the
|
|
398
|
+
chat saying why, and on Windows delivered by a detached, console-less helper
|
|
399
|
+
(`src/shared/notifyWorker.js`) because SnoreToast writes its diagnostics to the
|
|
400
|
+
attached console — the one blessed is drawing on — when notifications are
|
|
401
|
+
disabled for the application
|
|
309
402
|
|
|
310
403
|
#### `src/client/FileTransfer.js` — File Transfer
|
|
311
404
|
- Sending: reads the file, splits it into 48KB chunks, encrypts each chunk E2E via broadcast
|
|
@@ -433,9 +526,9 @@ securelan-chat/
|
|
|
433
526
|
Defines the protocol's message types. All messages have:
|
|
434
527
|
```js
|
|
435
528
|
{
|
|
436
|
-
type: string, //
|
|
437
|
-
version: 1, //
|
|
438
|
-
timestamp: number // Date.now()
|
|
529
|
+
type: string, // message type
|
|
530
|
+
version: 1, // protocol version
|
|
531
|
+
timestamp: number // Date.now() on the sender
|
|
439
532
|
}
|
|
440
533
|
```
|
|
441
534
|
|
|
@@ -520,7 +613,7 @@ export const FILE_CHUNK_SIZE = 49152; // 48KB
|
|
|
520
613
|
"version": 1,
|
|
521
614
|
"timestamp": 1739800000000,
|
|
522
615
|
"nickname": "Alice",
|
|
523
|
-
"publicKey": "base64(32
|
|
616
|
+
"publicKey": "base64(32-byte Curve25519 public key)",
|
|
524
617
|
"caps": ["sk1"]
|
|
525
618
|
}
|
|
526
619
|
```
|
|
@@ -541,7 +634,7 @@ client's.
|
|
|
541
634
|
{
|
|
542
635
|
"sessionId": "660e8400-e29b-41d4-a716-446655440001",
|
|
543
636
|
"nickname": "Bob",
|
|
544
|
-
"publicKey": "base64(
|
|
637
|
+
"publicKey": "base64(Bob's public key)",
|
|
545
638
|
"caps": ["sk1"]
|
|
546
639
|
}
|
|
547
640
|
],
|
|
@@ -666,7 +759,7 @@ Besides text messages, the encrypted payload may contain commands (the `action`
|
|
|
666
759
|
"peer": {
|
|
667
760
|
"sessionId": "770e8400-e29b-41d4-a716-446655440002",
|
|
668
761
|
"nickname": "Charlie",
|
|
669
|
-
"publicKey": "base64(
|
|
762
|
+
"publicKey": "base64(Charlie's public key)"
|
|
670
763
|
}
|
|
671
764
|
}
|
|
672
765
|
```
|
|
@@ -719,12 +812,12 @@ This combination (known as **NaCl crypto_box**) was chosen because:
|
|
|
719
812
|
### 6.3 Key Generation
|
|
720
813
|
|
|
721
814
|
```
|
|
722
|
-
1.
|
|
723
|
-
2. sodium.crypto_box_keypair()
|
|
724
|
-
- publicKey: 32 bytes (
|
|
725
|
-
- secretKey: 32 bytes (
|
|
726
|
-
3.
|
|
727
|
-
4. Fingerprint = SHA256(publicKey)
|
|
815
|
+
1. The client starts
|
|
816
|
+
2. sodium.crypto_box_keypair() produces:
|
|
817
|
+
- publicKey: 32 bytes (safe to share)
|
|
818
|
+
- secretKey: 32 bytes (NEVER leaves the process's memory)
|
|
819
|
+
3. Both held in sodium.sodium_malloc() (secure memory)
|
|
820
|
+
4. Fingerprint = SHA256(publicKey), formatted as XXXX:XXXX:XXXX:XXXX
|
|
728
821
|
```
|
|
729
822
|
|
|
730
823
|
### 6.4 Authenticated Encryption with crypto_box_easy
|
|
@@ -734,12 +827,12 @@ This combination (known as **NaCl crypto_box**) was chosen because:
|
|
|
734
827
|
```
|
|
735
828
|
crypto_box_easy(ciphertext, plaintext, nonce, recipientPublicKey, senderSecretKey)
|
|
736
829
|
|
|
737
|
-
|
|
738
|
-
1. X25519 DH:
|
|
830
|
+
Internally:
|
|
831
|
+
1. X25519 DH: sharedSecret = ECDH(recipientPub, senderSec)
|
|
739
832
|
2. Key derivation: encKey = HSalsa20(sharedSecret, zeros)
|
|
740
|
-
3.
|
|
741
|
-
4. MAC:
|
|
742
|
-
5. Output:
|
|
833
|
+
3. Encrypt: XSalsa20(plaintext, nonce, encKey) -> ciphertext
|
|
834
|
+
4. MAC: Poly1305(ciphertext) -> 16-byte tag
|
|
835
|
+
5. Output: tag || ciphertext (authenticated)
|
|
743
836
|
```
|
|
744
837
|
|
|
745
838
|
The shared key is derived implicitly on each call. The DH guarantees
|
|
@@ -749,38 +842,38 @@ that both sides (Alice and Bob) arrive at the same secret without exchanging it
|
|
|
749
842
|
|
|
750
843
|
```
|
|
751
844
|
Input:
|
|
752
|
-
- plaintext: Buffer (
|
|
753
|
-
- nonce: 24 bytes (
|
|
754
|
-
- recipientPublicKey: 32 bytes (
|
|
755
|
-
- senderSecretKey: 32 bytes (
|
|
845
|
+
- plaintext: Buffer (UTF-8 message)
|
|
846
|
+
- nonce: 24 bytes (from the NonceManager)
|
|
847
|
+
- recipientPublicKey: 32 bytes (recipient's public key)
|
|
848
|
+
- senderSecretKey: 32 bytes (sender's secret key)
|
|
756
849
|
|
|
757
|
-
|
|
850
|
+
Process:
|
|
758
851
|
ciphertext = crypto_box_easy(plaintext, nonce, recipientPublicKey, senderSecretKey)
|
|
759
852
|
|
|
760
853
|
Output:
|
|
761
|
-
- ciphertext: Buffer (plaintext.length + 16 bytes
|
|
762
|
-
- nonce: 24 bytes (
|
|
854
|
+
- ciphertext: Buffer (plaintext.length + 16 bytes of MAC)
|
|
855
|
+
- nonce: 24 bytes (sent alongside; it is not a secret)
|
|
763
856
|
|
|
764
|
-
Total
|
|
857
|
+
Total on the wire: ciphertext (N+16 bytes) + nonce (24 bytes)
|
|
765
858
|
```
|
|
766
859
|
|
|
767
860
|
### 6.6 Message Decryption
|
|
768
861
|
|
|
769
862
|
```
|
|
770
863
|
Input:
|
|
771
|
-
- ciphertext: Buffer (
|
|
772
|
-
- nonce: 24 bytes (
|
|
773
|
-
- senderPublicKey: 32 bytes (
|
|
774
|
-
- recipientSecretKey: 32 bytes (
|
|
864
|
+
- ciphertext: Buffer (off the network)
|
|
865
|
+
- nonce: 24 bytes (off the network)
|
|
866
|
+
- senderPublicKey: 32 bytes (sender's public key)
|
|
867
|
+
- recipientSecretKey: 32 bytes (recipient's secret key)
|
|
775
868
|
|
|
776
|
-
|
|
777
|
-
1. NonceManager
|
|
869
|
+
Process:
|
|
870
|
+
1. NonceManager checks the nonce has not been seen before (anti-replay)
|
|
778
871
|
2. plaintext = crypto_box_open_easy(ciphertext, nonce, senderPublicKey, recipientSecretKey)
|
|
779
|
-
3.
|
|
780
|
-
4.
|
|
872
|
+
3. MAC invalid -> reject (the message was tampered with)
|
|
873
|
+
4. MAC valid -> parse the inner JSON
|
|
781
874
|
|
|
782
875
|
Output:
|
|
783
|
-
- plaintext: Buffer (
|
|
876
|
+
- plaintext: Buffer (the original message)
|
|
784
877
|
```
|
|
785
878
|
|
|
786
879
|
### 6.7 Nonce Structure (24 bytes)
|
|
@@ -788,12 +881,12 @@ Output:
|
|
|
788
881
|
```
|
|
789
882
|
┌──────────────────┬──────────────┬──────────────────────┐
|
|
790
883
|
│ Timestamp (8B) │ Counter (4B) │ Random (12B) │
|
|
791
|
-
│
|
|
884
|
+
│ ms since epoch │ sequential │ sodium.randombytes │
|
|
792
885
|
└──────────────────┴──────────────┴──────────────────────┘
|
|
793
886
|
|
|
794
|
-
- Timestamp:
|
|
795
|
-
- Counter:
|
|
796
|
-
- Random:
|
|
887
|
+
- Timestamp: blocks replay across different sessions
|
|
888
|
+
- Counter: guarantees ordering and uniqueness within a session
|
|
889
|
+
- Random: guarantees uniqueness even with synchronised clocks
|
|
797
890
|
```
|
|
798
891
|
|
|
799
892
|
### 6.8 Fingerprint Verification
|
|
@@ -801,16 +894,16 @@ Output:
|
|
|
801
894
|
The fingerprint lets users verify each other's identity **out of band** (for example, in person or by phone):
|
|
802
895
|
|
|
803
896
|
```
|
|
804
|
-
1. Alice
|
|
805
|
-
2. Bob
|
|
806
|
-
3. Bob
|
|
807
|
-
4.
|
|
897
|
+
1. Alice reads her own fingerprint: A1B2:C3D4:E5F6:7890
|
|
898
|
+
2. Bob reads Alice's fingerprint: A1B2:C3D4:E5F6:7890
|
|
899
|
+
3. Bob confirms with Alice, in person, that the values match
|
|
900
|
+
4. They do not match -> MITM detected
|
|
808
901
|
```
|
|
809
902
|
|
|
810
903
|
The fingerprint is computed like this:
|
|
811
904
|
```
|
|
812
905
|
fingerprint = SHA-256(publicKey)
|
|
813
|
-
=
|
|
906
|
+
= first 8 bytes, hex, ':'-separated
|
|
814
907
|
= "A1B2:C3D4:E5F6:7890"
|
|
815
908
|
```
|
|
816
909
|
|
|
@@ -930,19 +1023,25 @@ The all-members rule is what keeps the damage on that side of the line.
|
|
|
930
1023
|
|---|---|---|
|
|
931
1024
|
| `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
1025
|
| `sk1` | relay | I can fan a room-addressed `group_message` out to a room's members. |
|
|
1026
|
+
| `dl1` | client | I can read a signed device list handed to me over the pairwise channel. Client-only: a device list rides the sealed channel the relay already carries, so there is no relay half to negotiate. |
|
|
1027
|
+
|
|
1028
|
+
Neither means "I send group messages". Receive and fan-out shipped a release
|
|
1029
|
+
ahead of send — 2.11.0 and 2.12.0 — because the switch is *every member agrees*:
|
|
1030
|
+
if the ability to read had arrived with the ability to write, the switch would
|
|
1031
|
+
only ever be true in rooms where everybody upgraded at the same moment, which on
|
|
1032
|
+
a public hub is close to never.
|
|
933
1033
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
which on a public hub is close to never. See `docs/design/sender-keys-on-relay.md`.
|
|
1034
|
+
The per-peer loop the switch falls back to is **not** going away; `/room` reports
|
|
1035
|
+
which path a room is on and why. See `docs/design/sender-keys-on-relay.md` for
|
|
1036
|
+
that decision, and `docs/design/multi-device.md` for the change that would
|
|
1037
|
+
multiply the fallback's cost by the number of devices per peer.
|
|
939
1038
|
|
|
940
1039
|
## 7. Handshake Protocol
|
|
941
1040
|
|
|
942
1041
|
### 7.1 Full Diagram
|
|
943
1042
|
|
|
944
1043
|
```
|
|
945
|
-
|
|
1044
|
+
Client A Server Client B
|
|
946
1045
|
│ │ │
|
|
947
1046
|
│ 1. JOIN(nick, pubKeyA) │ │
|
|
948
1047
|
│ ──────────────────────────>│ │
|
|
@@ -958,7 +1057,7 @@ which on a public hub is close to never. See `docs/design/sender-keys-on-relay.m
|
|
|
958
1057
|
│ usando pubKeyB + secKeyA │ usando pubKeyA + secKeyB
|
|
959
1058
|
│ │ │
|
|
960
1059
|
│ 6. ENCRYPTED_MSG ─────────│────────────────────────> │
|
|
961
|
-
│ │
|
|
1060
|
+
│ │ 7. Decrypt with sharedKey │
|
|
962
1061
|
│ │ │
|
|
963
1062
|
```
|
|
964
1063
|
|
|
@@ -989,24 +1088,24 @@ which on a public hub is close to never. See `docs/design/sender-keys-on-relay.m
|
|
|
989
1088
|
|
|
990
1089
|
## 8. Step-by-Step Communication Flow
|
|
991
1090
|
|
|
992
|
-
### 8.1 Full Scenario: Alice sends "
|
|
1091
|
+
### 8.1 Full Scenario: Alice sends "Hi" to Bob
|
|
993
1092
|
|
|
994
1093
|
```
|
|
995
|
-
|
|
1094
|
+
TIME ACTION
|
|
996
1095
|
───── ──────────────────────────────────────────────────────
|
|
997
|
-
t0 Alice
|
|
1096
|
+
t0 Alice types "Hi" in the composer and presses Enter
|
|
998
1097
|
|
|
999
|
-
t1 ChatController
|
|
1000
|
-
ChatController
|
|
1001
|
-
|
|
1098
|
+
t1 ChatController receives the text from the UI
|
|
1099
|
+
ChatController checks it has a sharedKey with Bob
|
|
1100
|
+
If it does not -> error "Handshake not completed with Bob"
|
|
1002
1101
|
|
|
1003
1102
|
t2 MessageCrypto.encrypt():
|
|
1004
|
-
- NonceManager
|
|
1005
|
-
-
|
|
1103
|
+
- NonceManager produces a 24-byte nonce
|
|
1104
|
+
- Serialises the inner payload: { text: "Hi", sentAt: t2, messageId: "a1b2" }
|
|
1006
1105
|
- crypto_box_easy_afternm(payload, nonce, sharedKeyAB)
|
|
1007
|
-
-
|
|
1106
|
+
- Returns { ciphertext: Buffer, nonce: Buffer }
|
|
1008
1107
|
|
|
1009
|
-
t3 Connection
|
|
1108
|
+
t3 Connection sends to the server:
|
|
1010
1109
|
{
|
|
1011
1110
|
type: "encrypted_message",
|
|
1012
1111
|
from: "alice-session-id",
|
|
@@ -1014,27 +1113,27 @@ t3 Connection envia ao servidor:
|
|
|
1014
1113
|
payload: { ciphertext: "base64(...)", nonce: "base64(...)" }
|
|
1015
1114
|
}
|
|
1016
1115
|
|
|
1017
|
-
t4
|
|
1018
|
-
-
|
|
1019
|
-
-
|
|
1020
|
-
-
|
|
1021
|
-
-
|
|
1116
|
+
t4 Server (MessageRouter):
|
|
1117
|
+
- Validates the structure (has type, from, to, payload)
|
|
1118
|
+
- Does NOT open the payload
|
|
1119
|
+
- Finds Bob's WebSocket by sessionId
|
|
1120
|
+
- Forwards the whole JSON to Bob
|
|
1022
1121
|
|
|
1023
|
-
t5 Bob (Connection)
|
|
1024
|
-
ChatController
|
|
1122
|
+
t5 Bob (Connection) receives the JSON
|
|
1123
|
+
ChatController identifies it: encrypted_message from Alice
|
|
1025
1124
|
|
|
1026
1125
|
t6 MessageCrypto.decrypt():
|
|
1027
|
-
-
|
|
1028
|
-
- NonceManager
|
|
1126
|
+
- Extracts ciphertext and nonce from the payload
|
|
1127
|
+
- NonceManager checks the nonce (not repeated, counter valid)
|
|
1029
1128
|
- crypto_box_open_easy_afternm(ciphertext, nonce, sharedKeyAB)
|
|
1030
|
-
-
|
|
1031
|
-
-
|
|
1129
|
+
- MAC fails -> reject (corrupted or tampered with)
|
|
1130
|
+
- MAC passes -> parse the inner JSON
|
|
1032
1131
|
|
|
1033
|
-
t7 ChatController
|
|
1034
|
-
|
|
1132
|
+
t7 ChatController receives { text: "Hi", sentAt: t2, messageId: "a1b2" }
|
|
1133
|
+
Checks sentAt is reasonable (not far in the past or future)
|
|
1035
1134
|
|
|
1036
|
-
t8 UI.
|
|
1037
|
-
Bob
|
|
1135
|
+
t8 UI.addMessage("Alice", "Hi")
|
|
1136
|
+
Bob sees the message under an "10:30 🦊 Alice" header
|
|
1038
1137
|
```
|
|
1039
1138
|
|
|
1040
1139
|
### 8.2 Scenario: Group Chat (broadcast)
|
|
@@ -1076,45 +1175,47 @@ a server change and is not wired up there yet.)
|
|
|
1076
1175
|
### 9.1 Server
|
|
1077
1176
|
|
|
1078
1177
|
```
|
|
1079
|
-
1.
|
|
1080
|
-
2.
|
|
1081
|
-
3.
|
|
1082
|
-
4.
|
|
1083
|
-
5.
|
|
1178
|
+
1. Load configuration and constants (config.js, constants.js)
|
|
1179
|
+
2. Run the preflight checks (port free, certs readable, limits sane)
|
|
1180
|
+
3. Load or generate the TLS certificate (CertManager) — wss:// by default
|
|
1181
|
+
4. Create the WebSocketServer on the configured port
|
|
1182
|
+
5. Create the SessionManager (empty session map) and the ConnectionGuard
|
|
1183
|
+
6. Create the MessageRouter (holding a reference to the SessionManager)
|
|
1184
|
+
7. Register handlers:
|
|
1084
1185
|
- on('connection') -> SessionManager.handleConnection()
|
|
1085
1186
|
- on('close') -> SessionManager.handleDisconnection()
|
|
1086
1187
|
- on('message') -> MessageRouter.route()
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
-
|
|
1090
|
-
-
|
|
1091
|
-
-
|
|
1092
|
-
|
|
1093
|
-
- IP local (todas as interfaces de rede)
|
|
1094
|
-
- Porta
|
|
1095
|
-
- "Servidor pronto. Clientes podem conectar em ws://<IP>:3600"
|
|
1188
|
+
8. Start the heartbeat interval (ping every client every 30s)
|
|
1189
|
+
9. Register SIGINT/SIGTERM for a graceful shutdown:
|
|
1190
|
+
- Notify every client
|
|
1191
|
+
- Close the connections
|
|
1192
|
+
- Release resources
|
|
1193
|
+
10. Print the banner: every local IP, the port, and the wss:// URLs
|
|
1096
1194
|
```
|
|
1097
1195
|
|
|
1098
1196
|
### 9.2 Client
|
|
1099
1197
|
|
|
1100
1198
|
```
|
|
1101
|
-
1.
|
|
1102
|
-
2.
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
-
|
|
1199
|
+
1. Show the CipherMesh banner
|
|
1200
|
+
2. First run only: the setup wizard (nickname, server, theme), saved to the
|
|
1201
|
+
config file so it never asks twice
|
|
1202
|
+
3. Ask for the nickname (1-20 chars, alphanumeric + underscore) unless saved
|
|
1203
|
+
4. Ask for the server address, or accept an invite string (default: localhost:3600)
|
|
1204
|
+
5. Restore the encrypted session state if there is one (StateManager)
|
|
1205
|
+
6. Generate or load the key pair (KeyManager) and the device identity
|
|
1206
|
+
7. Show the public-key fingerprint and its key art
|
|
1207
|
+
8. Connect over WebSocket, pinning the certificate (CertPinStore)
|
|
1208
|
+
9. Send JOIN (nickname + public key); wait for JOIN_ACK
|
|
1209
|
+
10. On error (duplicate nickname) -> ask for another one
|
|
1210
|
+
11. Receive the peer list and run the handshake with each, deriving a shared key
|
|
1211
|
+
12. Start the blessed UI: request the keyboard protocols, draw the header,
|
|
1212
|
+
the chat log, the status bar and the composer
|
|
1213
|
+
13. Enter the input loop
|
|
1214
|
+
14. Register a SIGINT handler to:
|
|
1215
|
+
- sodium_memzero() every key
|
|
1216
|
+
- Persist the encrypted state
|
|
1217
|
+
- Close the WebSocket
|
|
1218
|
+
- Restore the terminal (keyboard protocols, bracketed paste) and destroy the UI
|
|
1118
1219
|
```
|
|
1119
1220
|
|
|
1120
1221
|
---
|
|
@@ -1262,7 +1363,7 @@ routing). With sealed sender, the *sender* side of the social graph stays hidden
|
|
|
1262
1363
|
```
|
|
1263
1364
|
Startup:
|
|
1264
1365
|
1. Se existe estado salvo → prompt passphrase → loadState() → restaura KeyManager, Handshake, peers
|
|
1265
|
-
2.
|
|
1366
|
+
2. If there is none → optional passphrase prompt (to protect a future session)
|
|
1266
1367
|
|
|
1267
1368
|
Shutdown (Ctrl+C, /quit):
|
|
1268
1369
|
Se passphrase definida → serializeState() → saveState() cifrado
|
|
@@ -1310,48 +1411,49 @@ Shutdown (Ctrl+C, /quit):
|
|
|
1310
1411
|
**Future evolution** — P2P with a DHT (for larger networks):
|
|
1311
1412
|
```
|
|
1312
1413
|
1. Distributed Hash Table para discovery
|
|
1313
|
-
2.
|
|
1314
|
-
3.
|
|
1315
|
-
4.
|
|
1414
|
+
2. Each node keeps a partial routing table
|
|
1415
|
+
3. Messages can be routed over multiple hops
|
|
1416
|
+
4. Redundancy and fault tolerance
|
|
1316
1417
|
```
|
|
1317
1418
|
|
|
1318
|
-
### 11.6
|
|
1419
|
+
### 11.6 Project Infrastructure
|
|
1420
|
+
|
|
1421
|
+
Most of this section used to be a wishlist. It is now a status list, which is a
|
|
1422
|
+
better thing for it to be — what is left is the short part.
|
|
1423
|
+
|
|
1424
|
+
**In place**:
|
|
1319
1425
|
|
|
1320
|
-
**Repository structure**:
|
|
1321
1426
|
```
|
|
1322
|
-
|
|
1323
|
-
├──
|
|
1324
|
-
│ ├──
|
|
1325
|
-
│
|
|
1326
|
-
│
|
|
1327
|
-
│
|
|
1328
|
-
│ ├──
|
|
1329
|
-
│
|
|
1330
|
-
│
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
├── docs/
|
|
1334
|
-
│ ├── ARCHITECTURE.md
|
|
1335
|
-
│ ├── SECURITY.md # Politica de seguranca
|
|
1336
|
-
│ ├── CONTRIBUTING.md # Guia de contribuicao
|
|
1337
|
-
│ └── PROTOCOL.md # Especificacao do protocolo
|
|
1338
|
-
├── LICENSE # MIT ou Apache-2.0
|
|
1339
|
-
├── CHANGELOG.md # Historico de mudancas (semver)
|
|
1340
|
-
├── CODE_OF_CONDUCT.md
|
|
1341
|
-
└── SECURITY.md # Como reportar vulnerabilidades
|
|
1427
|
+
.github/
|
|
1428
|
+
├── workflows/
|
|
1429
|
+
│ ├── ci.yml # lint + format + tests, Node 20 and 22, on every PR
|
|
1430
|
+
│ ├── codeql.yml # CodeQL static analysis
|
|
1431
|
+
│ ├── release.yml # validate -> npm publish (OIDC) -> GitHub Release -> relay deploy
|
|
1432
|
+
│ ├── binaries.yml # standalone binaries
|
|
1433
|
+
│ ├── docker-publish.yml # image to GHCR
|
|
1434
|
+
│ ├── deploy.yml # site deploy, called by docker-publish
|
|
1435
|
+
│ └── hub-monitor.yml # scheduled hub health checks
|
|
1436
|
+
├── dependabot.yml
|
|
1437
|
+
└── CODEOWNERS
|
|
1342
1438
|
```
|
|
1343
1439
|
|
|
1344
|
-
|
|
1345
|
-
-
|
|
1346
|
-
|
|
1347
|
-
-
|
|
1348
|
-
-
|
|
1349
|
-
-
|
|
1350
|
-
|
|
1351
|
-
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1440
|
+
- Semantic versioning, and conventional commits enforced by commitlint
|
|
1441
|
+
- `LICENSE` (MIT), `SECURITY.md` with a disclosure process, `CONTRIBUTING.md`,
|
|
1442
|
+
`TERMS.md`
|
|
1443
|
+
- `CHANGELOG.md`, an entry per release
|
|
1444
|
+
- Badges in the README
|
|
1445
|
+
- npm publishing over **OIDC Trusted Publishing** — no long-lived token exists to
|
|
1446
|
+
leak
|
|
1447
|
+
- A Homebrew formula (`Formula/ciphermesh.rb`), whose digest is filled in after
|
|
1448
|
+
the tag publishes the tarball
|
|
1449
|
+
|
|
1450
|
+
**Not done, and worth doing**:
|
|
1451
|
+
|
|
1452
|
+
- Issue and pull-request templates
|
|
1453
|
+
- A code of conduct
|
|
1454
|
+
- GPG-signed release artefacts (the npm provenance attestation covers part of
|
|
1455
|
+
this, but not the GitHub Release assets)
|
|
1456
|
+
- Published API documentation
|
|
1355
1457
|
|
|
1356
1458
|
### 11.7 Other Improvements
|
|
1357
1459
|
|