ciphermesh 1.2.0 → 2.0.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 +5 -2
- package/README.pt-BR.md +5 -2
- package/docs/ARCHITECTURE.md +17 -9
- package/package.json +1 -1
- package/src/client/ChatController.js +46 -5
- package/src/p2p/P2PChatController.js +2 -0
- package/src/protocol/messages.js +7 -0
- package/src/protocol/validators.js +9 -24
- package/src/server/MessageRouter.js +3 -1
- package/src/server/WebSocketServer.js +5 -2
- package/src/shared/constants.js +1 -1
package/README.md
CHANGED
|
@@ -42,8 +42,8 @@ forwarding, survives CGNAT).
|
|
|
42
42
|
|-----|---------|----------|
|
|
43
43
|
| 🔐 | **Real E2EE** | Curve25519 + XSalsa20-Poly1305 via libsodium, keys in `sodium_malloc` — never touch disk |
|
|
44
44
|
| 🔄 | **Perfect Forward Secrecy** | Double Ratchet: one key per message, compromise today ≠ read yesterday |
|
|
45
|
-
| 🕶️ | **Metadata resistance** |
|
|
46
|
-
| 🕵️ | **TOFU + SAS** | Key-change detection (MITM alarm)
|
|
45
|
+
| 🕶️ | **Metadata resistance** | **Sealed sender** — the relay never sees who sent a message — plus fixed-bucket length padding on every ciphertext and opt-in cover traffic (`/cover`) |
|
|
46
|
+
| 🕵️ | **TOFU + SAS** | Key-change detection (MITM alarm), 6-digit voice-verifiable codes, and inline **✓/✗** trust badges next to names |
|
|
47
47
|
| 🌐 | **LAN & internet** | Auto-detects Tailscale, shows the reachable address in the banner |
|
|
48
48
|
| 📨 | **Invites with QR** | `/invite` prints a `ciphermesh://` string + QR — paste it, you're in the right room |
|
|
49
49
|
| ✓✓ | **Encrypted read receipts** | The ✓✓ travels as ordinary ciphertext — the server can't tell it apart |
|
|
@@ -116,6 +116,7 @@ QR code) to whoever you want to pull in.
|
|
|
116
116
|
| Command | Description |
|
|
117
117
|
|---------|-------------|
|
|
118
118
|
| `/help` | All commands |
|
|
119
|
+
| `/tips` | Show a rotating security/UX tip |
|
|
119
120
|
| `/users` | Who's online (with away/status) |
|
|
120
121
|
| `/msg <nick> <text>` | Private message (DM) |
|
|
121
122
|
| `/reply <text>` | Reply quoting the last received message |
|
|
@@ -156,6 +157,8 @@ QR code) to whoever you want to pull in.
|
|
|
156
157
|
| `/receipts [on\|off]` | Send read receipts (✓✓) |
|
|
157
158
|
| `/audit [n]` | Local audit log |
|
|
158
159
|
|
|
160
|
+
A green **✓** next to a name marks a SAS-verified peer; a red **✗** flags a key that changed since you last saw it (possible MITM). A newly-arrived unverified peer triggers a one-time reminder to `/verify` them.
|
|
161
|
+
|
|
159
162
|
</details>
|
|
160
163
|
|
|
161
164
|
<details>
|
package/README.pt-BR.md
CHANGED
|
@@ -42,8 +42,8 @@ forwarding, imune a CGNAT).
|
|
|
42
42
|
|-----|---------|--------|
|
|
43
43
|
| 🔐 | **E2EE de verdade** | Curve25519 + XSalsa20-Poly1305 via libsodium, chaves em `sodium_malloc` — nunca tocam o disco |
|
|
44
44
|
| 🔄 | **Perfect Forward Secrecy** | Double Ratchet: uma chave por mensagem — comprometer hoje ≠ ler ontem |
|
|
45
|
-
| 🕶️ | **Resistência a metadados** |
|
|
46
|
-
| 🕵️ | **TOFU + SAS** | Alarme de troca de chave (MITM)
|
|
45
|
+
| 🕶️ | **Resistência a metadados** | **Sealed sender** — o relay nunca vê quem enviou a mensagem — + padding de comprimento em buckets fixos em todo ciphertext e cover traffic opcional (`/cover`) |
|
|
46
|
+
| 🕵️ | **TOFU + SAS** | Alarme de troca de chave (MITM), código de 6 dígitos verificável por voz e badges de confiança **✓/✗** inline ao lado dos nomes |
|
|
47
47
|
| 🌐 | **LAN e internet** | Detecta Tailscale sozinho e mostra o endereço alcançável no banner |
|
|
48
48
|
| 📨 | **Convites com QR** | `/invite` gera uma string `ciphermesh://` + QR — colou, caiu na sala certa |
|
|
49
49
|
| ✓✓ | **Read receipts cifrados** | O ✓✓ viaja como ciphertext comum — o servidor não distingue de mensagem |
|
|
@@ -116,6 +116,7 @@ quem você quiser puxar pra conversa.
|
|
|
116
116
|
| Comando | Descrição |
|
|
117
117
|
|---------|-----------|
|
|
118
118
|
| `/help` | Todos os comandos |
|
|
119
|
+
| `/tips` | Mostra uma dica rotativa de segurança/UX |
|
|
119
120
|
| `/users` | Quem está online (com away/status) |
|
|
120
121
|
| `/msg <nick> <texto>` | Mensagem privada (DM) |
|
|
121
122
|
| `/reply <texto>` | Responde citando a última mensagem recebida |
|
|
@@ -156,6 +157,8 @@ quem você quiser puxar pra conversa.
|
|
|
156
157
|
| `/receipts [on\|off]` | Envio de confirmação de leitura (✓✓) |
|
|
157
158
|
| `/audit [n]` | Log de auditoria local |
|
|
158
159
|
|
|
160
|
+
Um **✓** verde ao lado de um nome indica um peer verificado por SAS; um **✗** vermelho sinaliza uma chave que mudou desde a última vez (possível MITM). Um peer novo não-verificado dispara um lembrete único para `/verify`.
|
|
161
|
+
|
|
159
162
|
</details>
|
|
160
163
|
|
|
161
164
|
<details>
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -533,18 +533,14 @@ export const FILE_CHUNK_SIZE = 49152; // 48KB
|
|
|
533
533
|
```json
|
|
534
534
|
{
|
|
535
535
|
"type": "encrypted_message",
|
|
536
|
-
"version":
|
|
536
|
+
"version": 2,
|
|
537
537
|
"timestamp": 1739800001000,
|
|
538
|
-
"from": "550e8400-e29b-41d4-a716-446655440000",
|
|
539
538
|
"to": "660e8400-e29b-41d4-a716-446655440001",
|
|
540
|
-
"
|
|
541
|
-
"ciphertext": "base64(mensagem cifrada com crypto_box_easy)",
|
|
542
|
-
"nonce": "base64(24 bytes do nonce usado)"
|
|
543
|
-
}
|
|
539
|
+
"sealed": "base64( crypto_box_seal({ from, payload }, recipientPublicKey) )"
|
|
544
540
|
}
|
|
545
541
|
```
|
|
546
542
|
|
|
547
|
-
**Note**: The `
|
|
543
|
+
**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.)
|
|
548
544
|
|
|
549
545
|
### 5.4 Decrypted content (never travels in cleartext)
|
|
550
546
|
|
|
@@ -939,17 +935,29 @@ a server change and is not wired up there yet.)
|
|
|
939
935
|
| **Denial of Service** | Rate limiting + maxPayload | Partial |
|
|
940
936
|
| **Forward secrecy** | Double Ratchet — a unique key per message | Mitigated |
|
|
941
937
|
| **Metadata analysis** | Message padding + fixed sizes | Partial |
|
|
938
|
+
| **Relay learns who SENT each message** | Sealed sender — the sender + payload are sealed to the recipient; the relay routes by recipient only and never stamps, stores, or logs a sender | Partial (honest relay) |
|
|
942
939
|
|
|
943
940
|
### 10.2 What the server CAN deduce (metadata)
|
|
944
941
|
|
|
945
942
|
Even without reading content, the server knows:
|
|
946
|
-
- **Who** is online
|
|
947
|
-
- **
|
|
943
|
+
- **Who** is online (the roster: nicknames + public keys, from JOIN)
|
|
944
|
+
- **To whom** each message is routed (the recipient) — *the **sender** is hidden by sealed sender; an honest relay never learns who sent a given message*
|
|
948
945
|
- **When** messages are sent — *mitigated by cover traffic (optional)*
|
|
949
946
|
- **Approximate size** of messages — *mitigated: only the padding bucket leaks*
|
|
950
947
|
- **Frequency** of communication — *mitigated by cover traffic (optional)*
|
|
951
948
|
|
|
952
949
|
Implemented mitigations:
|
|
950
|
+
- **Sealed sender** (`crypto/SealedSender.js`, protocol v2): every
|
|
951
|
+
`encrypted_message` carries only `to` + an opaque `sealed` blob. The sender's
|
|
952
|
+
identity and the inner payload are wrapped in a libsodium `crypto_box_seal`
|
|
953
|
+
(anonymous box) to the recipient's key; the relay routes by recipient and
|
|
954
|
+
**never stamps, stores, or logs who sent a message**. *Honest guarantee &
|
|
955
|
+
limits:* this protects against an honest-but-curious relay, offline-queue
|
|
956
|
+
compromise, and network observers — the same model as Signal's sealed sender.
|
|
957
|
+
It does **not** anonymise the sender against a *malicious* relay that
|
|
958
|
+
correlates the sending socket (inherent to a persistent authenticated
|
|
959
|
+
connection), the **recipient** is still visible (routing needs it), and P2P
|
|
960
|
+
mode has no relay at all.
|
|
953
961
|
- **Length padding** (`MessageCrypto.padMessage`, buckets
|
|
954
962
|
`[128..32768]`): applied on all three encryption paths (static, ratchet, and
|
|
955
963
|
deniable) before encrypting. The server sees only which bucket, not the real size.
|
package/package.json
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
createJoin,
|
|
10
10
|
createEncryptedMessage,
|
|
11
11
|
createRatchetedMessage,
|
|
12
|
+
createSealedMessage,
|
|
12
13
|
createKeyUpdate,
|
|
13
14
|
createChangeRoom,
|
|
14
15
|
createListRooms,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
createBanPeer,
|
|
18
19
|
ERR,
|
|
19
20
|
} from '../protocol/messages.js';
|
|
21
|
+
import { sealEnvelope, openEnvelope } from '../crypto/SealedSender.js';
|
|
20
22
|
import { KEY_ROTATION_INTERVAL_MS, EMOJI_MAP, COVER_CONSTANT_MS } from '../shared/constants.js';
|
|
21
23
|
import { KeyManager } from '../crypto/KeyManager.js';
|
|
22
24
|
import { Handshake } from '../crypto/Handshake.js';
|
|
@@ -507,6 +509,19 @@ export class ChatController {
|
|
|
507
509
|
|
|
508
510
|
// ── Received encrypted message ────────────────────────────────
|
|
509
511
|
#onEncryptedMessage(msg) {
|
|
512
|
+
// Sealed sender: the relay handed us only `to` + an opaque blob. Open it
|
|
513
|
+
// with our identity key to recover the real sender + payload; from here the
|
|
514
|
+
// rest of the handler is unchanged. A blob that isn't for us (or is tampered)
|
|
515
|
+
// simply fails to open and is dropped.
|
|
516
|
+
if (typeof msg.sealed === 'string') {
|
|
517
|
+
const opened = this.#openSealed(msg.sealed);
|
|
518
|
+
if (!opened || typeof opened.from !== 'string' || !opened.payload) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
// Rebind to a fresh object — never mutate the received message.
|
|
522
|
+
msg = { ...msg, from: opened.from, payload: opened.payload };
|
|
523
|
+
}
|
|
524
|
+
|
|
510
525
|
const peer = this.#peers.get(msg.from);
|
|
511
526
|
if (!peer) {
|
|
512
527
|
this.#ui.addErrorMessage('Message from unknown peer');
|
|
@@ -908,6 +923,7 @@ export class ChatController {
|
|
|
908
923
|
case '/help':
|
|
909
924
|
this.#ui.addInfoMessage('Available commands:');
|
|
910
925
|
this.#ui.addInfoMessage(' /help - Show this help');
|
|
926
|
+
this.#ui.addInfoMessage(' /tips - Show a security/UX tip');
|
|
911
927
|
this.#ui.addInfoMessage(' /users - List online users');
|
|
912
928
|
this.#ui.addInfoMessage(' /msg <nick> <text> - Send a private message (DM)');
|
|
913
929
|
this.#ui.addInfoMessage(' /reply <text> - Reply to the last received message');
|
|
@@ -925,6 +941,7 @@ export class ChatController {
|
|
|
925
941
|
this.#ui.addInfoMessage(' /verify-confirm <nick> - Confirm peer verification');
|
|
926
942
|
this.#ui.addInfoMessage(" /trust <nick> - Accept a peer's new key");
|
|
927
943
|
this.#ui.addInfoMessage(" /trustlist - Peers' trust status");
|
|
944
|
+
this.#ui.addInfoMessage(' (✓ = verified peer · ✗ = key changed — shown next to a name)');
|
|
928
945
|
this.#ui.addInfoMessage(' /clear - Clear the chat');
|
|
929
946
|
this.#ui.addInfoMessage(' /file <path> - Send a file (max 50MB)');
|
|
930
947
|
this.#ui.addInfoMessage(
|
|
@@ -2119,6 +2136,28 @@ export class ChatController {
|
|
|
2119
2136
|
}
|
|
2120
2137
|
}
|
|
2121
2138
|
|
|
2139
|
+
// Sealed sender: wrap an outgoing wire message so the relay sees only `to` and
|
|
2140
|
+
// an opaque blob. The sender identity (`from`) + the payload are sealed to the
|
|
2141
|
+
// recipient's key — only they can open it.
|
|
2142
|
+
#sealAndSend(recipientPublicKey, wireMsg) {
|
|
2143
|
+
const sealed = sealEnvelope(wireMsg.from, wireMsg.payload, recipientPublicKey);
|
|
2144
|
+
this.#connection.send(createSealedMessage(wireMsg.to, sealed));
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
// Open a sealed envelope with our identity key, falling back to the previous
|
|
2148
|
+
// key during the post-rotation grace window. Returns { from, payload } or null.
|
|
2149
|
+
#openSealed(sealedB64) {
|
|
2150
|
+
let opened = openEnvelope(sealedB64, this.#keyManager.publicKey, this.#keyManager.secretKey);
|
|
2151
|
+
if (!opened && this.#keyManager.previousPublicKey) {
|
|
2152
|
+
opened = openEnvelope(
|
|
2153
|
+
sealedB64,
|
|
2154
|
+
this.#keyManager.previousPublicKey,
|
|
2155
|
+
this.#keyManager.previousSecretKey,
|
|
2156
|
+
);
|
|
2157
|
+
}
|
|
2158
|
+
return opened;
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2122
2161
|
// ── Send encrypted payload to a single peer ────────────────────
|
|
2123
2162
|
#sendPayloadToPeer(peerId, payload) {
|
|
2124
2163
|
const peerPublicKey = this.#handshake.getPeerPublicKey(peerId);
|
|
@@ -2130,7 +2169,7 @@ export class ChatController {
|
|
|
2130
2169
|
if (ratchet && ratchet.isInitialized) {
|
|
2131
2170
|
try {
|
|
2132
2171
|
const result = ratchet.encrypt(payload);
|
|
2133
|
-
this.#
|
|
2172
|
+
this.#sealAndSend(peerPublicKey, createRatchetedMessage(this.#sessionId, peerId, result));
|
|
2134
2173
|
return;
|
|
2135
2174
|
} catch {
|
|
2136
2175
|
// Fall through to static path
|
|
@@ -2144,7 +2183,8 @@ export class ChatController {
|
|
|
2144
2183
|
peerPublicKey,
|
|
2145
2184
|
this.#handshake.secretKey,
|
|
2146
2185
|
);
|
|
2147
|
-
this.#
|
|
2186
|
+
this.#sealAndSend(
|
|
2187
|
+
peerPublicKey,
|
|
2148
2188
|
createEncryptedMessage(
|
|
2149
2189
|
this.#sessionId,
|
|
2150
2190
|
peerId,
|
|
@@ -2260,7 +2300,7 @@ export class ChatController {
|
|
|
2260
2300
|
nonce.toString('base64'),
|
|
2261
2301
|
);
|
|
2262
2302
|
msg.payload.deniable = true;
|
|
2263
|
-
this.#
|
|
2303
|
+
this.#sealAndSend(peerPublicKey, msg);
|
|
2264
2304
|
continue;
|
|
2265
2305
|
}
|
|
2266
2306
|
|
|
@@ -2269,7 +2309,7 @@ export class ChatController {
|
|
|
2269
2309
|
if (ratchet && ratchet.isInitialized) {
|
|
2270
2310
|
try {
|
|
2271
2311
|
const result = ratchet.encrypt(payload);
|
|
2272
|
-
this.#
|
|
2312
|
+
this.#sealAndSend(peerPublicKey, createRatchetedMessage(this.#sessionId, peerId, result));
|
|
2273
2313
|
continue;
|
|
2274
2314
|
} catch {
|
|
2275
2315
|
// Ratchet failed — fall through to static path
|
|
@@ -2285,7 +2325,8 @@ export class ChatController {
|
|
|
2285
2325
|
this.#handshake.secretKey,
|
|
2286
2326
|
);
|
|
2287
2327
|
|
|
2288
|
-
this.#
|
|
2328
|
+
this.#sealAndSend(
|
|
2329
|
+
peerPublicKey,
|
|
2289
2330
|
createEncryptedMessage(
|
|
2290
2331
|
this.#sessionId,
|
|
2291
2332
|
peerId,
|
|
@@ -677,6 +677,7 @@ export class P2PChatController {
|
|
|
677
677
|
case '/help':
|
|
678
678
|
this.#ui.addInfoMessage('Available commands (P2P mode):');
|
|
679
679
|
this.#ui.addInfoMessage(' /help - Show this help');
|
|
680
|
+
this.#ui.addInfoMessage(' /tips - Show a security/UX tip');
|
|
680
681
|
this.#ui.addInfoMessage(' /users - List connected peers');
|
|
681
682
|
this.#ui.addInfoMessage(' /msg <nick> <text> - Send a private message (DM)');
|
|
682
683
|
this.#ui.addInfoMessage(' /fingerprint - Show your fingerprint');
|
|
@@ -685,6 +686,7 @@ export class P2PChatController {
|
|
|
685
686
|
this.#ui.addInfoMessage(' /verify-confirm <nick> - Confirm verification');
|
|
686
687
|
this.#ui.addInfoMessage(' /trust <nick> - Accept a new key');
|
|
687
688
|
this.#ui.addInfoMessage(' /trustlist - Trust status');
|
|
689
|
+
this.#ui.addInfoMessage(' (✓ = verified peer · ✗ = key changed — shown next to a name)');
|
|
688
690
|
this.#ui.addInfoMessage(' /clear - Clear the chat');
|
|
689
691
|
this.#ui.addInfoMessage(' /file <path> - Send a file (max 50MB)');
|
|
690
692
|
this.#ui.addInfoMessage(
|
package/src/protocol/messages.js
CHANGED
|
@@ -81,6 +81,13 @@ export function createRatchetedMessage(from, to, payload) {
|
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
// Sealed-sender envelope (protocol v2): the relay sees only the recipient and an
|
|
85
|
+
// opaque blob. The sender's identity + the inner payload are sealed to the
|
|
86
|
+
// recipient's key (see crypto/SealedSender.js). There is deliberately no `from`.
|
|
87
|
+
export function createSealedMessage(to, sealedB64) {
|
|
88
|
+
return { ...base(MSG.ENCRYPTED_MESSAGE), to, sealed: sealedB64 };
|
|
89
|
+
}
|
|
90
|
+
|
|
84
91
|
export function createError(code, message) {
|
|
85
92
|
return { ...base(MSG.ERROR), code, message };
|
|
86
93
|
}
|
|
@@ -86,33 +86,18 @@ export function validateJoin(msg) {
|
|
|
86
86
|
return { valid: true, nickname: nick };
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
// Sealed sender (protocol v2): the relay only ever sees the recipient and an
|
|
90
|
+
// opaque sealed blob — never the sender or the inner payload. So there is
|
|
91
|
+
// nothing to validate here beyond routing target + a non-empty base64 envelope
|
|
92
|
+
// (the overall size is already capped in validateMessage). The recipient's
|
|
93
|
+
// unseal is fail-safe on garbage, so the relay needn't inspect further.
|
|
89
94
|
export function validateEncryptedMessage(msg) {
|
|
90
|
-
if (!isString(msg.
|
|
91
|
-
return { valid: false, error: 'Missing
|
|
95
|
+
if (!isString(msg.to)) {
|
|
96
|
+
return { valid: false, error: 'Missing to field' };
|
|
92
97
|
}
|
|
93
|
-
if (!
|
|
94
|
-
return { valid: false, error: 'Missing
|
|
98
|
+
if (!isValidBase64(msg.sealed)) {
|
|
99
|
+
return { valid: false, error: 'Missing or invalid sealed envelope' };
|
|
95
100
|
}
|
|
96
|
-
if (!isString(msg.payload.ciphertext) || !isString(msg.payload.nonce)) {
|
|
97
|
-
return { valid: false, error: 'Invalid payload structure' };
|
|
98
|
-
}
|
|
99
|
-
if (!isValidBase64(msg.payload.nonce, 24)) {
|
|
100
|
-
return { valid: false, error: 'Invalid nonce' };
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Ratcheted message: validate extra fields
|
|
104
|
-
if (msg.payload.ephemeralPublicKey !== undefined) {
|
|
105
|
-
if (!isValidBase64(msg.payload.ephemeralPublicKey, PUBLIC_KEY_SIZE)) {
|
|
106
|
-
return { valid: false, error: 'Invalid ephemeral public key' };
|
|
107
|
-
}
|
|
108
|
-
if (!Number.isInteger(msg.payload.counter) || msg.payload.counter < 0) {
|
|
109
|
-
return { valid: false, error: 'Invalid counter' };
|
|
110
|
-
}
|
|
111
|
-
if (!Number.isInteger(msg.payload.previousCounter) || msg.payload.previousCounter < 0) {
|
|
112
|
-
return { valid: false, error: 'Invalid previousCounter' };
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
101
|
return { valid: true };
|
|
117
102
|
}
|
|
118
103
|
|
|
@@ -53,7 +53,9 @@ export class MessageRouter {
|
|
|
53
53
|
|
|
54
54
|
if (recipientSession.ws.readyState === 1) {
|
|
55
55
|
recipientSession.ws.send(JSON.stringify(msg));
|
|
56
|
-
log
|
|
56
|
+
// Sealed sender: never log who sent it — only that something was routed to
|
|
57
|
+
// the recipient. Correlating sender->recipient in logs would defeat it.
|
|
58
|
+
log.debug(`routed -> ${msg.to.slice(0, 8)}`);
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
|
|
@@ -293,8 +293,11 @@ export class SecureWSServer {
|
|
|
293
293
|
return;
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
-
//
|
|
297
|
-
|
|
296
|
+
// Sealed sender: the relay routes purely by `to`. It deliberately does NOT
|
|
297
|
+
// learn, stamp, store, or log who sent this — the sender's identity is
|
|
298
|
+
// sealed inside the envelope for the recipient only. Strip any stray `from`
|
|
299
|
+
// a client might set so it can never be forwarded.
|
|
300
|
+
delete msg.from;
|
|
298
301
|
|
|
299
302
|
this.#messageRouter.route(ws.sessionId, msg);
|
|
300
303
|
}
|
package/src/shared/constants.js
CHANGED