dandelion-mesh 1.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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +192 -0
  3. package/build/main/crypto/CryptoService.d.ts +31 -0
  4. package/build/main/crypto/CryptoService.js +78 -0
  5. package/build/main/index.d.ts +11 -0
  6. package/build/main/index.js +24 -0
  7. package/build/main/mesh/DandelionMesh.d.ts +115 -0
  8. package/build/main/mesh/DandelionMesh.js +368 -0
  9. package/build/main/mesh/types.d.ts +58 -0
  10. package/build/main/mesh/types.js +3 -0
  11. package/build/main/raft/RaftNode.d.ts +87 -0
  12. package/build/main/raft/RaftNode.js +443 -0
  13. package/build/main/raft/log/InMemoryRaftLog.d.ts +20 -0
  14. package/build/main/raft/log/InMemoryRaftLog.js +55 -0
  15. package/build/main/raft/log/LocalStorageRaftLog.d.ts +9 -0
  16. package/build/main/raft/log/LocalStorageRaftLog.js +16 -0
  17. package/build/main/raft/log/RaftLog.d.ts +30 -0
  18. package/build/main/raft/log/RaftLog.js +3 -0
  19. package/build/main/raft/log/RaftLog.test-util.d.ts +3 -0
  20. package/build/main/raft/log/RaftLog.test-util.js +82 -0
  21. package/build/main/raft/log/SessionStorageRaftLog.d.ts +9 -0
  22. package/build/main/raft/log/SessionStorageRaftLog.js +38 -0
  23. package/build/main/raft/log/StorageRaftLog.d.ts +28 -0
  24. package/build/main/raft/log/StorageRaftLog.js +117 -0
  25. package/build/main/raft/log/StorageRaftLog.test-util.d.ts +3 -0
  26. package/build/main/raft/log/StorageRaftLog.test-util.js +54 -0
  27. package/build/main/raft/types.d.ts +53 -0
  28. package/build/main/raft/types.js +3 -0
  29. package/build/main/transport/PeerJSTransport.d.ts +46 -0
  30. package/build/main/transport/PeerJSTransport.js +139 -0
  31. package/build/main/transport/Transport.d.ts +38 -0
  32. package/build/main/transport/Transport.js +8 -0
  33. package/build/module/crypto/CryptoService.d.ts +31 -0
  34. package/build/module/crypto/CryptoService.js +69 -0
  35. package/build/module/index.d.ts +11 -0
  36. package/build/module/index.js +11 -0
  37. package/build/module/mesh/DandelionMesh.d.ts +115 -0
  38. package/build/module/mesh/DandelionMesh.js +364 -0
  39. package/build/module/mesh/types.d.ts +58 -0
  40. package/build/module/mesh/types.js +2 -0
  41. package/build/module/raft/RaftNode.d.ts +87 -0
  42. package/build/module/raft/RaftNode.js +440 -0
  43. package/build/module/raft/log/InMemoryRaftLog.d.ts +20 -0
  44. package/build/module/raft/log/InMemoryRaftLog.js +48 -0
  45. package/build/module/raft/log/LocalStorageRaftLog.d.ts +9 -0
  46. package/build/module/raft/log/LocalStorageRaftLog.js +12 -0
  47. package/build/module/raft/log/RaftLog.d.ts +30 -0
  48. package/build/module/raft/log/RaftLog.js +2 -0
  49. package/build/module/raft/log/RaftLog.test-util.d.ts +3 -0
  50. package/build/module/raft/log/RaftLog.test-util.js +78 -0
  51. package/build/module/raft/log/SessionStorageRaftLog.d.ts +9 -0
  52. package/build/module/raft/log/SessionStorageRaftLog.js +34 -0
  53. package/build/module/raft/log/StorageRaftLog.d.ts +28 -0
  54. package/build/module/raft/log/StorageRaftLog.js +114 -0
  55. package/build/module/raft/log/StorageRaftLog.test-util.d.ts +3 -0
  56. package/build/module/raft/log/StorageRaftLog.test-util.js +50 -0
  57. package/build/module/raft/types.d.ts +53 -0
  58. package/build/module/raft/types.js +2 -0
  59. package/build/module/transport/PeerJSTransport.d.ts +46 -0
  60. package/build/module/transport/PeerJSTransport.js +134 -0
  61. package/build/module/transport/Transport.d.ts +38 -0
  62. package/build/module/transport/Transport.js +7 -0
  63. package/package.json +119 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Wenhao Ji <predator.ray@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,192 @@
1
+ # <img src="https://github.com/predatorray/dandelion-mesh/blob/assets/Dandelion_40x40.png?raw=true" alt="Description of image" width="40" height="40"> Dandelion Mesh
2
+
3
+ [![License](https://img.shields.io/github/license/predatorray/dandelion-mesh)](LICENSE)
4
+ [![Build Status](https://img.shields.io/github/actions/workflow/status/predatorray/dandelion-mesh/ci.yml?branch=main)](https://github.com/predatorray/dandelion-mesh/actions)
5
+ [![codecov](https://codecov.io/github/predatorray/dandelion-mesh/graph/badge.svg?token=KqwNV13MD4)](https://codecov.io/github/predatorray/dandelion-mesh)
6
+ ![NPM Version](https://img.shields.io/badge/npm-not_published-blue)
7
+
8
+ Serverless mesh network for browsers using WebRTC.
9
+
10
+ ***Connect***, ***Broadcast***, and ***Sync State*** without a central server.
11
+
12
+ ## Overview
13
+
14
+ `dandelion-mesh` is a fault-tolerant P2P service mesh library for browser applications.
15
+
16
+ It combines:
17
+ - WebRTC data channels for transport,
18
+ - RSA hybrid encryption for private messaging,
19
+ - and the Raft consensus algorithm for leader election and ordered log replication
20
+
21
+ All without requiring a dedicated server.
22
+
23
+ ## Usage
24
+
25
+ ### Install
26
+
27
+ ```bash
28
+ npm install dandelion-mesh # not published yet
29
+ ```
30
+
31
+ ### Basic example
32
+
33
+ ```ts
34
+ import { PeerJSTransport, DandelionMesh } from 'dandelion-mesh';
35
+
36
+ // Create a transport and mesh instance
37
+ const transport = new PeerJSTransport({ peerId: 'alice' });
38
+ const mesh = new DandelionMesh(transport, {
39
+ bootstrapPeers: ['bob', 'charlie'],
40
+ });
41
+
42
+ // Listen for events
43
+ mesh.on('ready', (id) => {
44
+ console.log('My peer ID:', id);
45
+ });
46
+
47
+ mesh.on('message', (msg) => {
48
+ if (msg.type === 'public') {
49
+ console.log(`[${msg.sender}]: ${JSON.stringify(msg.data)}`);
50
+ }
51
+ if (msg.type === 'private') {
52
+ console.log(`[private from ${msg.sender}]: ${JSON.stringify(msg.data)}`);
53
+ }
54
+ });
55
+
56
+ mesh.on('leaderChanged', (leaderId) => {
57
+ console.log('Current leader:', leaderId);
58
+ });
59
+
60
+ mesh.on('peersChanged', (peers) => {
61
+ console.log('Connected peers:', peers);
62
+ });
63
+
64
+ // Send messages
65
+ await mesh.sendPublic({ action: 'bet', amount: 100 });
66
+ await mesh.sendPrivate('bob', { cards: ['Ah', 'Kd'] });
67
+ ```
68
+
69
+ ### Using localStorage for durable sessions
70
+
71
+ ```ts
72
+ import {
73
+ PeerJSTransport,
74
+ DandelionMesh,
75
+ LocalStorageRaftLog,
76
+ } from 'dandelion-mesh';
77
+
78
+ const transport = new PeerJSTransport({ peerId: 'alice' });
79
+ const mesh = new DandelionMesh(transport, {
80
+ bootstrapPeers: ['bob', 'charlie'],
81
+ raftLog: new LocalStorageRaftLog('my-game-room'),
82
+ });
83
+
84
+ // Raft state (term, votedFor, log) persists across page refreshes,
85
+ // allowing a peer to rejoin and catch up from where it left off.
86
+ ```
87
+
88
+ ### Custom transport
89
+
90
+ ```ts
91
+ import { Transport, DandelionMesh } from 'dandelion-mesh';
92
+
93
+ class MyWebSocketTransport implements Transport {
94
+ // Implement the Transport interface with your own
95
+ // connection management and message passing logic.
96
+ // ...
97
+ }
98
+
99
+ const transport = new MyWebSocketTransport();
100
+ const mesh = new DandelionMesh(transport);
101
+ ```
102
+
103
+ ## High-level architecture
104
+
105
+ ```mermaid
106
+ graph TB
107
+ subgraph "dandelion-mesh"
108
+ direction TB
109
+ API["DandelionMesh API<br/><i>sendPublic() · sendPrivate() · on('message')</i>"]
110
+
111
+ subgraph layers [" "]
112
+ direction LR
113
+ RAFT["Raft Consensus<br/><i>Leader Election<br/>Log Replication</i>"]
114
+ CRYPTO["Crypto Service<br/><i>RSA-OAEP + AES-GCM<br/>Hybrid Encryption</i>"]
115
+ end
116
+
117
+ TRANSPORT["Transport Layer<br/><i>PeerJS (default) · pluggable</i>"]
118
+ end
119
+
120
+ API --> RAFT
121
+ API --> CRYPTO
122
+ CRYPTO --> RAFT
123
+ RAFT --> TRANSPORT
124
+ TRANSPORT <-->|WebRTC<br/>Data Channels| TRANSPORT
125
+ ```
126
+
127
+ ## Low-level architecture
128
+
129
+ ### Message flow
130
+
131
+ ```mermaid
132
+ sequenceDiagram
133
+ participant App as Application
134
+ participant Mesh as DandelionMesh
135
+ participant Raft as Raft Leader
136
+ participant Peers as Other Peers
137
+
138
+ Note over App,Peers: Public message
139
+ App->>Mesh: sendPublic(data)
140
+ Mesh->>Raft: propose(PublicMessageEntry)
141
+ Raft->>Peers: AppendEntries (log replication)
142
+ Peers-->>Raft: success (majority)
143
+ Raft->>Mesh: committed
144
+ Mesh->>App: on('message', PublicMessage)
145
+ Raft->>Peers: leaderCommit updated
146
+ Note over Peers: Each peer applies & emits 'message'
147
+
148
+ Note over App,Peers: Private message
149
+ App->>Mesh: sendPrivate(recipientId, data)
150
+ Mesh->>Mesh: encrypt with recipient's RSA public key
151
+ Mesh->>Raft: propose(EncryptedPrivateMessage)
152
+ Raft->>Peers: AppendEntries (encrypted payload in log)
153
+ Peers-->>Raft: success (majority)
154
+ Note over Peers: Only recipient can decrypt
155
+ ```
156
+
157
+ ### Leader election
158
+
159
+ ```mermaid
160
+ stateDiagram-v2
161
+ [*] --> Follower
162
+ Follower --> Candidate: election timeout<br/>(no heartbeat received)
163
+ Candidate --> Leader: received votes<br/>from majority
164
+ Candidate --> Candidate: election timeout<br/>(split vote)
165
+ Candidate --> Follower: discovered higher term<br/>or current leader
166
+ Leader --> Follower: discovered higher term
167
+ Leader --> Leader: sends heartbeats<br/>to prevent elections
168
+ ```
169
+
170
+ ### Key Design Decisions
171
+
172
+ - **Transport abstraction** — The `Transport` interface decouples the mesh from PeerJS. Any P2P transport (WebSocket, libp2p, etc.) can be plugged in by implementing the interface.
173
+
174
+ - **Raft consensus** — Full implementation per the [Raft paper](https://raft.github.io/raft.pdf):
175
+ - Leader election with randomized timeouts (2000–4000ms default)
176
+ - Log replication with AppendEntries consistency checks
177
+ - Commitment only for current-term entries (Figure 8 safety)
178
+ - Dynamic membership updates as peers join/leave
179
+
180
+ - **Three log backends** — `InMemoryRaftLog` for ephemeral sessions, `LocalStorageRaftLog` for peers that need to survive page refreshes and rejoin, and `SessionStorageRaftLog` for per-tab durability that clears when the tab is closed.
181
+
182
+ - **Hybrid encryption** — Private messages use RSA-OAEP to wrap a random AES-256-GCM key. Public keys are exchanged as Raft log entries, so every peer receives them through the same ordered replication path. All peers see the encrypted log entry, but only the intended recipient can decrypt it.
183
+
184
+ - **Ordered delivery via Raft** — All messages (public and encrypted private) go through Raft as log entries. Non-leader peers forward proposals to the leader. Once committed, public messages are delivered to all; encrypted messages are decrypted only by the intended recipient. This guarantees total ordering of all events across the cluster.
185
+
186
+ ## Support & Bug Report
187
+
188
+ If you find any bugs or have suggestions, please feel free to [open an issue](https://github.com/predatorray/dandelion-mesh/issues/new).
189
+
190
+ ## License
191
+
192
+ This project is licensed under the [MIT License](LICENSE).
@@ -0,0 +1,31 @@
1
+ /**
2
+ * RSA-OAEP + AES-GCM hybrid encryption service.
3
+ *
4
+ * RSA is used to encrypt a random AES session key, and AES-GCM encrypts the
5
+ * actual payload. This avoids RSA plaintext size limits while providing
6
+ * authenticated encryption for the message body.
7
+ */
8
+ export interface CryptoKeyBundle {
9
+ publicKey: CryptoKey;
10
+ privateKey: CryptoKey;
11
+ /** JWK export of the public key, suitable for broadcasting */
12
+ publicKeyJwk: JsonWebKey;
13
+ }
14
+ export interface EncryptedPayload {
15
+ /** RSA-encrypted AES key (hex) */
16
+ encryptedKey: string;
17
+ /** AES-GCM initialization vector (hex) */
18
+ iv: string;
19
+ /** AES-GCM ciphertext (hex) */
20
+ ciphertext: string;
21
+ }
22
+ /** Generate an RSA-OAEP key pair and export the public key as JWK */
23
+ export declare function generateKeyBundle(modulusLength?: number): Promise<CryptoKeyBundle>;
24
+ /** Import a peer's public key from its JWK representation */
25
+ export declare function importPublicKey(jwk: JsonWebKey): Promise<CryptoKey>;
26
+ /** Encrypt arbitrary data with a recipient's RSA public key (hybrid encryption) */
27
+ export declare function encrypt(plaintext: Uint8Array, recipientPublicKey: CryptoKey): Promise<EncryptedPayload>;
28
+ /** Decrypt a hybrid-encrypted payload with the recipient's RSA private key */
29
+ export declare function decrypt(payload: EncryptedPayload, privateKey: CryptoKey): Promise<Uint8Array>;
30
+ export declare function arrayBufferToHex(buffer: ArrayBuffer): string;
31
+ export declare function hexToArrayBuffer(hex: string): ArrayBuffer;
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ /**
3
+ * RSA-OAEP + AES-GCM hybrid encryption service.
4
+ *
5
+ * RSA is used to encrypt a random AES session key, and AES-GCM encrypts the
6
+ * actual payload. This avoids RSA plaintext size limits while providing
7
+ * authenticated encryption for the message body.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.hexToArrayBuffer = exports.arrayBufferToHex = exports.decrypt = exports.encrypt = exports.importPublicKey = exports.generateKeyBundle = void 0;
11
+ const AES_KEY_LENGTH = 256;
12
+ const AES_IV_LENGTH = 12;
13
+ const RSA_HASH = 'SHA-256';
14
+ /** Generate an RSA-OAEP key pair and export the public key as JWK */
15
+ async function generateKeyBundle(modulusLength = 4096) {
16
+ const keyPair = await crypto.subtle.generateKey({
17
+ name: 'RSA-OAEP',
18
+ modulusLength,
19
+ publicExponent: new Uint8Array([1, 0, 1]),
20
+ hash: RSA_HASH,
21
+ }, true, ['encrypt', 'decrypt']);
22
+ const publicKeyJwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey);
23
+ return {
24
+ publicKey: keyPair.publicKey,
25
+ privateKey: keyPair.privateKey,
26
+ publicKeyJwk,
27
+ };
28
+ }
29
+ exports.generateKeyBundle = generateKeyBundle;
30
+ /** Import a peer's public key from its JWK representation */
31
+ async function importPublicKey(jwk) {
32
+ return crypto.subtle.importKey('jwk', jwk, { name: 'RSA-OAEP', hash: RSA_HASH }, false, ['encrypt']);
33
+ }
34
+ exports.importPublicKey = importPublicKey;
35
+ /** Encrypt arbitrary data with a recipient's RSA public key (hybrid encryption) */
36
+ async function encrypt(plaintext, recipientPublicKey) {
37
+ // Generate random AES key
38
+ const aesKey = await crypto.subtle.generateKey({ name: 'AES-GCM', length: AES_KEY_LENGTH }, true, ['encrypt']);
39
+ // Encrypt the AES key with RSA
40
+ const rawAesKey = await crypto.subtle.exportKey('raw', aesKey);
41
+ const encryptedAesKey = await crypto.subtle.encrypt({ name: 'RSA-OAEP' }, recipientPublicKey, rawAesKey);
42
+ // Encrypt the plaintext with AES-GCM
43
+ const iv = crypto.getRandomValues(new Uint8Array(AES_IV_LENGTH));
44
+ const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, aesKey, plaintext);
45
+ return {
46
+ encryptedKey: arrayBufferToHex(encryptedAesKey),
47
+ iv: arrayBufferToHex(iv.buffer),
48
+ ciphertext: arrayBufferToHex(ciphertext),
49
+ };
50
+ }
51
+ exports.encrypt = encrypt;
52
+ /** Decrypt a hybrid-encrypted payload with the recipient's RSA private key */
53
+ async function decrypt(payload, privateKey) {
54
+ // Decrypt the AES key with RSA
55
+ const rawAesKey = await crypto.subtle.decrypt({ name: 'RSA-OAEP' }, privateKey, hexToArrayBuffer(payload.encryptedKey));
56
+ const aesKey = await crypto.subtle.importKey('raw', rawAesKey, { name: 'AES-GCM', length: AES_KEY_LENGTH }, false, ['decrypt']);
57
+ // Decrypt the ciphertext with AES-GCM
58
+ const iv = hexToArrayBuffer(payload.iv);
59
+ const plaintext = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, aesKey, hexToArrayBuffer(payload.ciphertext));
60
+ return new Uint8Array(plaintext);
61
+ }
62
+ exports.decrypt = decrypt;
63
+ // --- Hex conversion utilities ---
64
+ function arrayBufferToHex(buffer) {
65
+ return Array.from(new Uint8Array(buffer))
66
+ .map((b) => b.toString(16).padStart(2, '0'))
67
+ .join('');
68
+ }
69
+ exports.arrayBufferToHex = arrayBufferToHex;
70
+ function hexToArrayBuffer(hex) {
71
+ const bytes = new Uint8Array(hex.length / 2);
72
+ for (let i = 0; i < hex.length; i += 2) {
73
+ bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
74
+ }
75
+ return bytes.buffer;
76
+ }
77
+ exports.hexToArrayBuffer = hexToArrayBuffer;
78
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiQ3J5cHRvU2VydmljZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9jcnlwdG8vQ3J5cHRvU2VydmljZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUE7Ozs7OztHQU1HOzs7QUFFSCxNQUFNLGNBQWMsR0FBRyxHQUFHLENBQUM7QUFDM0IsTUFBTSxhQUFhLEdBQUcsRUFBRSxDQUFDO0FBQ3pCLE1BQU0sUUFBUSxHQUFHLFNBQVMsQ0FBQztBQWtCM0IscUVBQXFFO0FBQzlELEtBQUssVUFBVSxpQkFBaUIsQ0FDckMsYUFBYSxHQUFHLElBQUk7SUFFcEIsTUFBTSxPQUFPLEdBQUcsTUFBTSxNQUFNLENBQUMsTUFBTSxDQUFDLFdBQVcsQ0FDN0M7UUFDRSxJQUFJLEVBQUUsVUFBVTtRQUNoQixhQUFhO1FBQ2IsY0FBYyxFQUFFLElBQUksVUFBVSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQztRQUN6QyxJQUFJLEVBQUUsUUFBUTtLQUNmLEVBQ0QsSUFBSSxFQUNKLENBQUMsU0FBUyxFQUFFLFNBQVMsQ0FBQyxDQUN2QixDQUFDO0lBQ0YsTUFBTSxZQUFZLEdBQUcsTUFBTSxNQUFNLENBQUMsTUFBTSxDQUFDLFNBQVMsQ0FBQyxLQUFLLEVBQUUsT0FBTyxDQUFDLFNBQVMsQ0FBQyxDQUFDO0lBQzdFLE9BQU87UUFDTCxTQUFTLEVBQUUsT0FBTyxDQUFDLFNBQVM7UUFDNUIsVUFBVSxFQUFFLE9BQU8sQ0FBQyxVQUFVO1FBQzlCLFlBQVk7S0FDYixDQUFDO0FBQ0osQ0FBQztBQW5CRCw4Q0FtQkM7QUFFRCw2REFBNkQ7QUFDdEQsS0FBSyxVQUFVLGVBQWUsQ0FBQyxHQUFlO0lBQ25ELE9BQU8sTUFBTSxDQUFDLE1BQU0sQ0FBQyxTQUFTLENBQzVCLEtBQUssRUFDTCxHQUFHLEVBQ0gsRUFBRSxJQUFJLEVBQUUsVUFBVSxFQUFFLElBQUksRUFBRSxRQUFRLEVBQUUsRUFDcEMsS0FBSyxF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
@@ -0,0 +1,11 @@
1
+ export { Transport, TransportEvents, TransportEventName, } from './transport/Transport';
2
+ export { PeerJSTransport, PeerJSTransportOptions, PeerLike, DataConnectionLike, } from './transport/PeerJSTransport';
3
+ export { generateKeyBundle, importPublicKey, encrypt, decrypt, CryptoKeyBundle, EncryptedPayload, } from './crypto/CryptoService';
4
+ export { RaftNode, RaftNodeOptions } from './raft/RaftNode';
5
+ export { RaftRole, LogEntry, RequestVoteArgs, RequestVoteResult, AppendEntriesArgs, AppendEntriesResult, RaftMessage, RaftPersistentState, RaftNodeEvents, } from './raft/types';
6
+ export { RaftLog } from './raft/log/RaftLog';
7
+ export { InMemoryRaftLog } from './raft/log/InMemoryRaftLog';
8
+ export { LocalStorageRaftLog } from './raft/log/LocalStorageRaftLog';
9
+ export { SessionStorageRaftLog } from './raft/log/SessionStorageRaftLog';
10
+ export { DandelionMesh, DandelionMeshOptions } from './mesh/DandelionMesh';
11
+ export { PublicMessage, PrivateMessage, MeshMessage, DandelionMeshEvents, } from './mesh/types';
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DandelionMesh = exports.SessionStorageRaftLog = exports.LocalStorageRaftLog = exports.InMemoryRaftLog = exports.RaftNode = exports.decrypt = exports.encrypt = exports.importPublicKey = exports.generateKeyBundle = exports.PeerJSTransport = void 0;
4
+ var PeerJSTransport_1 = require("./transport/PeerJSTransport");
5
+ Object.defineProperty(exports, "PeerJSTransport", { enumerable: true, get: function () { return PeerJSTransport_1.PeerJSTransport; } });
6
+ // Crypto
7
+ var CryptoService_1 = require("./crypto/CryptoService");
8
+ Object.defineProperty(exports, "generateKeyBundle", { enumerable: true, get: function () { return CryptoService_1.generateKeyBundle; } });
9
+ Object.defineProperty(exports, "importPublicKey", { enumerable: true, get: function () { return CryptoService_1.importPublicKey; } });
10
+ Object.defineProperty(exports, "encrypt", { enumerable: true, get: function () { return CryptoService_1.encrypt; } });
11
+ Object.defineProperty(exports, "decrypt", { enumerable: true, get: function () { return CryptoService_1.decrypt; } });
12
+ // Raft consensus
13
+ var RaftNode_1 = require("./raft/RaftNode");
14
+ Object.defineProperty(exports, "RaftNode", { enumerable: true, get: function () { return RaftNode_1.RaftNode; } });
15
+ var InMemoryRaftLog_1 = require("./raft/log/InMemoryRaftLog");
16
+ Object.defineProperty(exports, "InMemoryRaftLog", { enumerable: true, get: function () { return InMemoryRaftLog_1.InMemoryRaftLog; } });
17
+ var LocalStorageRaftLog_1 = require("./raft/log/LocalStorageRaftLog");
18
+ Object.defineProperty(exports, "LocalStorageRaftLog", { enumerable: true, get: function () { return LocalStorageRaftLog_1.LocalStorageRaftLog; } });
19
+ var SessionStorageRaftLog_1 = require("./raft/log/SessionStorageRaftLog");
20
+ Object.defineProperty(exports, "SessionStorageRaftLog", { enumerable: true, get: function () { return SessionStorageRaftLog_1.SessionStorageRaftLog; } });
21
+ // Mesh (main API)
22
+ var DandelionMesh_1 = require("./mesh/DandelionMesh");
23
+ Object.defineProperty(exports, "DandelionMesh", { enumerable: true, get: function () { return DandelionMesh_1.DandelionMesh; } });
24
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBTUEsK0RBS3FDO0FBSm5DLGtIQUFBLGVBQWUsT0FBQTtBQU1qQixTQUFTO0FBQ1Qsd0RBT2dDO0FBTjlCLGtIQUFBLGlCQUFpQixPQUFBO0FBQ2pCLGdIQUFBLGVBQWUsT0FBQTtBQUNmLHdHQUFBLE9BQU8sT0FBQTtBQUNQLHdHQUFBLE9BQU8sT0FBQTtBQUtULGlCQUFpQjtBQUNqQiw0Q0FBNEQ7QUFBbkQsb0dBQUEsUUFBUSxPQUFBO0FBYWpCLDhEQUE2RDtBQUFwRCxrSEFBQSxlQUFlLE9BQUE7QUFDeEIsc0VBQXFFO0FBQTVELDBIQUFBLG1CQUFtQixPQUFBO0FBQzVCLDBFQUF5RTtBQUFoRSw4SEFBQSxxQkFBcUIsT0FBQTtBQUU5QixrQkFBa0I7QUFDbEIsc0RBQTJFO0FBQWxFLDhHQUFBLGFBQWEsT0FBQSJ9
@@ -0,0 +1,115 @@
1
+ import { CryptoKeyBundle } from '../crypto/CryptoService';
2
+ import { RaftNodeOptions } from '../raft/RaftNode';
3
+ import { RaftLog } from '../raft/log/RaftLog';
4
+ import { Transport } from '../transport/Transport';
5
+ import { DandelionMeshEvents, MeshLogCommand } from './types';
6
+ export interface DandelionMeshOptions {
7
+ /** RSA modulus length in bits (default 4096) */
8
+ modulusLength?: number;
9
+ /** Raft configuration */
10
+ raft?: RaftNodeOptions;
11
+ /** Raft log implementation (default: InMemoryRaftLog) */
12
+ raftLog?: RaftLog<MeshLogCommand>;
13
+ /** Known peer IDs to connect to on startup */
14
+ bootstrapPeers?: string[];
15
+ /**
16
+ * Pre-generated crypto key bundle. When provided, the mesh skips key
17
+ * generation and uses these keys directly. Accepts either a resolved
18
+ * bundle or a Promise (e.g. from a prior `generateKeyBundle()` call).
19
+ * If omitted, a new key pair is generated automatically.
20
+ */
21
+ cryptoKeyBundle?: CryptoKeyBundle | Promise<CryptoKeyBundle>;
22
+ }
23
+ /**
24
+ * DandelionMesh — a fault-tolerant P2P mesh network for browser applications.
25
+ *
26
+ * Combines three layers:
27
+ * - **Transport** (default: PeerJS) — handles peer-to-peer WebRTC connections.
28
+ * - **Crypto** — RSA-OAEP + AES-256-GCM hybrid encryption for private messages.
29
+ * - **Raft consensus** — leader election and totally-ordered log replication.
30
+ *
31
+ * All application messages (public broadcasts and encrypted private messages)
32
+ * flow through the Raft log, guaranteeing every peer sees the same events in
33
+ * the same order. Non-leader peers forward proposals to the current leader.
34
+ *
35
+ * ## Events
36
+ *
37
+ * | Event | Payload | When |
38
+ * |------------------|--------------------------------------|---------------------------------------------|
39
+ * | `ready` | `localPeerId: string` | Transport is open and Raft has started |
40
+ * | `message` | `MeshMessage<T>, replay: boolean` | A committed log entry is delivered |
41
+ * | `peersChanged` | `peers: string[]` | A peer connects or disconnects |
42
+ * | `leaderChanged` | `leaderId: string \| null` | Raft leader changes |
43
+ * | `error` | `Error` | Transport-level error |
44
+ *
45
+ * @typeParam T - The application-level payload type for messages.
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * const transport = new PeerJSTransport({ peerId: 'alice' });
50
+ * const mesh = new DandelionMesh<GameAction>(transport, {
51
+ * bootstrapPeers: ['bob', 'charlie'],
52
+ * });
53
+ *
54
+ * mesh.on('ready', (id) => console.log('My ID:', id));
55
+ * mesh.on('message', (msg) => {
56
+ * if (msg.type === 'public') console.log(msg.sender, msg.data);
57
+ * if (msg.type === 'private') console.log('secret from', msg.sender);
58
+ * });
59
+ *
60
+ * await mesh.sendPublic({ action: 'bet', amount: 100 });
61
+ * await mesh.sendPrivate('bob', { cards: ['Ah', 'Kd'] });
62
+ * ```
63
+ *
64
+ * @example Providing a pre-generated crypto key bundle
65
+ * ```ts
66
+ * const bundle = await generateKeyBundle(2048);
67
+ * const mesh = new DandelionMesh(transport, { cryptoKeyBundle: bundle });
68
+ * ```
69
+ */
70
+ export declare class DandelionMesh<T = unknown> {
71
+ private readonly transport;
72
+ private readonly raftLog;
73
+ private raftNode;
74
+ private localPeerId;
75
+ private readonly cryptoReady;
76
+ private cryptoBundle;
77
+ private readonly peerPublicKeys;
78
+ private readonly pendingKeyWaiters;
79
+ private readonly modulusLength;
80
+ private readonly raftOptions;
81
+ private readonly bootstrapPeers;
82
+ private lastAppliedIndex;
83
+ private readonly listeners;
84
+ constructor(transport: Transport, options?: DandelionMeshOptions);
85
+ /** Send a public (broadcast) message through the Raft cluster */
86
+ sendPublic(data: T): Promise<boolean>;
87
+ /** Send an encrypted private message through the Raft cluster */
88
+ sendPrivate(recipientPeerId: string, data: T): Promise<boolean>;
89
+ /** Get all connected peer IDs (including self) */
90
+ get peers(): string[];
91
+ /** Get the current Raft leader ID */
92
+ get leaderId(): string | null;
93
+ /** Whether this node is the Raft leader */
94
+ get isLeader(): boolean;
95
+ /** The local peer ID (available after 'ready') */
96
+ get peerId(): string | undefined;
97
+ /** Register an event listener */
98
+ on<E extends keyof DandelionMeshEvents<T>>(event: E, listener: DandelionMeshEvents<T>[E]): void;
99
+ /** Remove an event listener */
100
+ off<E extends keyof DandelionMeshEvents<T>>(event: E, listener: DandelionMeshEvents<T>[E]): void;
101
+ /** Shut down the mesh: stop Raft, close transport */
102
+ close(): void;
103
+ private onTransportOpen;
104
+ private onPeerConnected;
105
+ private onPeerDisconnected;
106
+ private onTransportMessage;
107
+ private onTransportError;
108
+ private handleControlMessage;
109
+ private onRaftCommitted;
110
+ private handleEncryptedCommit;
111
+ private proposePublicKey;
112
+ private getOrAwaitPublicKey;
113
+ private wireMessage;
114
+ private emit;
115
+ }