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.
- package/LICENSE +21 -0
- package/README.md +192 -0
- package/build/main/crypto/CryptoService.d.ts +31 -0
- package/build/main/crypto/CryptoService.js +78 -0
- package/build/main/index.d.ts +11 -0
- package/build/main/index.js +24 -0
- package/build/main/mesh/DandelionMesh.d.ts +115 -0
- package/build/main/mesh/DandelionMesh.js +368 -0
- package/build/main/mesh/types.d.ts +58 -0
- package/build/main/mesh/types.js +3 -0
- package/build/main/raft/RaftNode.d.ts +87 -0
- package/build/main/raft/RaftNode.js +443 -0
- package/build/main/raft/log/InMemoryRaftLog.d.ts +20 -0
- package/build/main/raft/log/InMemoryRaftLog.js +55 -0
- package/build/main/raft/log/LocalStorageRaftLog.d.ts +9 -0
- package/build/main/raft/log/LocalStorageRaftLog.js +16 -0
- package/build/main/raft/log/RaftLog.d.ts +30 -0
- package/build/main/raft/log/RaftLog.js +3 -0
- package/build/main/raft/log/RaftLog.test-util.d.ts +3 -0
- package/build/main/raft/log/RaftLog.test-util.js +82 -0
- package/build/main/raft/log/SessionStorageRaftLog.d.ts +9 -0
- package/build/main/raft/log/SessionStorageRaftLog.js +38 -0
- package/build/main/raft/log/StorageRaftLog.d.ts +28 -0
- package/build/main/raft/log/StorageRaftLog.js +117 -0
- package/build/main/raft/log/StorageRaftLog.test-util.d.ts +3 -0
- package/build/main/raft/log/StorageRaftLog.test-util.js +54 -0
- package/build/main/raft/types.d.ts +53 -0
- package/build/main/raft/types.js +3 -0
- package/build/main/transport/PeerJSTransport.d.ts +46 -0
- package/build/main/transport/PeerJSTransport.js +139 -0
- package/build/main/transport/Transport.d.ts +38 -0
- package/build/main/transport/Transport.js +8 -0
- package/build/module/crypto/CryptoService.d.ts +31 -0
- package/build/module/crypto/CryptoService.js +69 -0
- package/build/module/index.d.ts +11 -0
- package/build/module/index.js +11 -0
- package/build/module/mesh/DandelionMesh.d.ts +115 -0
- package/build/module/mesh/DandelionMesh.js +364 -0
- package/build/module/mesh/types.d.ts +58 -0
- package/build/module/mesh/types.js +2 -0
- package/build/module/raft/RaftNode.d.ts +87 -0
- package/build/module/raft/RaftNode.js +440 -0
- package/build/module/raft/log/InMemoryRaftLog.d.ts +20 -0
- package/build/module/raft/log/InMemoryRaftLog.js +48 -0
- package/build/module/raft/log/LocalStorageRaftLog.d.ts +9 -0
- package/build/module/raft/log/LocalStorageRaftLog.js +12 -0
- package/build/module/raft/log/RaftLog.d.ts +30 -0
- package/build/module/raft/log/RaftLog.js +2 -0
- package/build/module/raft/log/RaftLog.test-util.d.ts +3 -0
- package/build/module/raft/log/RaftLog.test-util.js +78 -0
- package/build/module/raft/log/SessionStorageRaftLog.d.ts +9 -0
- package/build/module/raft/log/SessionStorageRaftLog.js +34 -0
- package/build/module/raft/log/StorageRaftLog.d.ts +28 -0
- package/build/module/raft/log/StorageRaftLog.js +114 -0
- package/build/module/raft/log/StorageRaftLog.test-util.d.ts +3 -0
- package/build/module/raft/log/StorageRaftLog.test-util.js +50 -0
- package/build/module/raft/types.d.ts +53 -0
- package/build/module/raft/types.js +2 -0
- package/build/module/transport/PeerJSTransport.d.ts +46 -0
- package/build/module/transport/PeerJSTransport.js +134 -0
- package/build/module/transport/Transport.d.ts +38 -0
- package/build/module/transport/Transport.js +7 -0
- 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)
|
|
4
|
+
[](https://github.com/predatorray/dandelion-mesh/actions)
|
|
5
|
+
[](https://codecov.io/github/predatorray/dandelion-mesh)
|
|
6
|
+

|
|
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
|
+
}
|