@meshwhisper/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +138 -0
- package/dist/browser/index.d.ts +4 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +19 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/chaff/index.d.ts +91 -0
- package/dist/chaff/index.d.ts.map +1 -0
- package/dist/chaff/index.js +268 -0
- package/dist/chaff/index.js.map +1 -0
- package/dist/cluster/index.d.ts +159 -0
- package/dist/cluster/index.d.ts.map +1 -0
- package/dist/cluster/index.js +393 -0
- package/dist/cluster/index.js.map +1 -0
- package/dist/compliance/index.d.ts +129 -0
- package/dist/compliance/index.d.ts.map +1 -0
- package/dist/compliance/index.js +315 -0
- package/dist/compliance/index.js.map +1 -0
- package/dist/crypto/index.d.ts +65 -0
- package/dist/crypto/index.d.ts.map +1 -0
- package/dist/crypto/index.js +146 -0
- package/dist/crypto/index.js.map +1 -0
- package/dist/group/index.d.ts +155 -0
- package/dist/group/index.d.ts.map +1 -0
- package/dist/group/index.js +560 -0
- package/dist/group/index.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/namespace/index.d.ts +155 -0
- package/dist/namespace/index.d.ts.map +1 -0
- package/dist/namespace/index.js +278 -0
- package/dist/namespace/index.js.map +1 -0
- package/dist/node/index.d.ts +4 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +19 -0
- package/dist/node/index.js.map +1 -0
- package/dist/packet/index.d.ts +63 -0
- package/dist/packet/index.d.ts.map +1 -0
- package/dist/packet/index.js +244 -0
- package/dist/packet/index.js.map +1 -0
- package/dist/permissions/index.d.ts +107 -0
- package/dist/permissions/index.d.ts.map +1 -0
- package/dist/permissions/index.js +282 -0
- package/dist/permissions/index.js.map +1 -0
- package/dist/persistence/idb-storage.d.ts +27 -0
- package/dist/persistence/idb-storage.d.ts.map +1 -0
- package/dist/persistence/idb-storage.js +75 -0
- package/dist/persistence/idb-storage.js.map +1 -0
- package/dist/persistence/index.d.ts +4 -0
- package/dist/persistence/index.d.ts.map +1 -0
- package/dist/persistence/index.js +3 -0
- package/dist/persistence/index.js.map +1 -0
- package/dist/persistence/node-storage.d.ts +33 -0
- package/dist/persistence/node-storage.d.ts.map +1 -0
- package/dist/persistence/node-storage.js +90 -0
- package/dist/persistence/node-storage.js.map +1 -0
- package/dist/persistence/serialization.d.ts +4 -0
- package/dist/persistence/serialization.d.ts.map +1 -0
- package/dist/persistence/serialization.js +49 -0
- package/dist/persistence/serialization.js.map +1 -0
- package/dist/persistence/types.d.ts +29 -0
- package/dist/persistence/types.d.ts.map +1 -0
- package/dist/persistence/types.js +5 -0
- package/dist/persistence/types.js.map +1 -0
- package/dist/ratchet/index.d.ts +80 -0
- package/dist/ratchet/index.d.ts.map +1 -0
- package/dist/ratchet/index.js +259 -0
- package/dist/ratchet/index.js.map +1 -0
- package/dist/reciprocity/index.d.ts +109 -0
- package/dist/reciprocity/index.d.ts.map +1 -0
- package/dist/reciprocity/index.js +311 -0
- package/dist/reciprocity/index.js.map +1 -0
- package/dist/relay/index.d.ts +87 -0
- package/dist/relay/index.d.ts.map +1 -0
- package/dist/relay/index.js +286 -0
- package/dist/relay/index.js.map +1 -0
- package/dist/routing/index.d.ts +136 -0
- package/dist/routing/index.d.ts.map +1 -0
- package/dist/routing/index.js +478 -0
- package/dist/routing/index.js.map +1 -0
- package/dist/sdk/index.d.ts +322 -0
- package/dist/sdk/index.d.ts.map +1 -0
- package/dist/sdk/index.js +1530 -0
- package/dist/sdk/index.js.map +1 -0
- package/dist/sybil/index.d.ts +123 -0
- package/dist/sybil/index.d.ts.map +1 -0
- package/dist/sybil/index.js +491 -0
- package/dist/sybil/index.js.map +1 -0
- package/dist/transport/browser/index.d.ts +34 -0
- package/dist/transport/browser/index.d.ts.map +1 -0
- package/dist/transport/browser/index.js +176 -0
- package/dist/transport/browser/index.js.map +1 -0
- package/dist/transport/local/index.d.ts +57 -0
- package/dist/transport/local/index.d.ts.map +1 -0
- package/dist/transport/local/index.js +442 -0
- package/dist/transport/local/index.js.map +1 -0
- package/dist/transport/negotiator/index.d.ts +79 -0
- package/dist/transport/negotiator/index.d.ts.map +1 -0
- package/dist/transport/negotiator/index.js +289 -0
- package/dist/transport/negotiator/index.js.map +1 -0
- package/dist/transport/node/index.d.ts +56 -0
- package/dist/transport/node/index.d.ts.map +1 -0
- package/dist/transport/node/index.js +209 -0
- package/dist/transport/node/index.js.map +1 -0
- package/dist/transport/noop/index.d.ts +11 -0
- package/dist/transport/noop/index.d.ts.map +1 -0
- package/dist/transport/noop/index.js +20 -0
- package/dist/transport/noop/index.js.map +1 -0
- package/dist/transport/p2p/index.d.ts +109 -0
- package/dist/transport/p2p/index.d.ts.map +1 -0
- package/dist/transport/p2p/index.js +237 -0
- package/dist/transport/p2p/index.js.map +1 -0
- package/dist/transport/websocket/index.d.ts +89 -0
- package/dist/transport/websocket/index.d.ts.map +1 -0
- package/dist/transport/websocket/index.js +498 -0
- package/dist/transport/websocket/index.js.map +1 -0
- package/dist/transport/websocket/serialize.d.ts +5 -0
- package/dist/transport/websocket/serialize.d.ts.map +1 -0
- package/dist/transport/websocket/serialize.js +55 -0
- package/dist/transport/websocket/serialize.js.map +1 -0
- package/dist/types.d.ts +215 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -0
- package/dist/x3dh/index.d.ts +120 -0
- package/dist/x3dh/index.d.ts.map +1 -0
- package/dist/x3dh/index.js +290 -0
- package/dist/x3dh/index.js.map +1 -0
- package/package.json +59 -0
- package/src/browser/index.ts +19 -0
- package/src/chaff/index.ts +340 -0
- package/src/cluster/index.ts +482 -0
- package/src/compliance/index.ts +407 -0
- package/src/crypto/index.ts +193 -0
- package/src/group/index.ts +719 -0
- package/src/index.ts +87 -0
- package/src/lz4js.d.ts +58 -0
- package/src/namespace/index.ts +336 -0
- package/src/node/index.ts +19 -0
- package/src/packet/index.ts +326 -0
- package/src/permissions/index.ts +405 -0
- package/src/persistence/idb-storage.ts +83 -0
- package/src/persistence/index.ts +3 -0
- package/src/persistence/node-storage.ts +96 -0
- package/src/persistence/serialization.ts +75 -0
- package/src/persistence/types.ts +33 -0
- package/src/ratchet/index.ts +363 -0
- package/src/reciprocity/index.ts +371 -0
- package/src/relay/index.ts +382 -0
- package/src/routing/index.ts +577 -0
- package/src/sdk/index.ts +1994 -0
- package/src/sybil/index.ts +661 -0
- package/src/transport/browser/index.ts +201 -0
- package/src/transport/local/index.ts +540 -0
- package/src/transport/negotiator/index.ts +397 -0
- package/src/transport/node/index.ts +234 -0
- package/src/transport/noop/index.ts +22 -0
- package/src/transport/p2p/index.ts +345 -0
- package/src/transport/websocket/index.ts +660 -0
- package/src/transport/websocket/serialize.ts +68 -0
- package/src/types.ts +275 -0
- package/src/x3dh/index.ts +388 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// MeshWhisper SDK — Contact Permissions Module
|
|
3
|
+
// Endpoint-side permission enforcement for P2P contact initiation.
|
|
4
|
+
// ============================================================
|
|
5
|
+
|
|
6
|
+
import type { PermissionModel } from '../types';
|
|
7
|
+
|
|
8
|
+
// --- Interfaces ---
|
|
9
|
+
|
|
10
|
+
export interface ContactContext {
|
|
11
|
+
senderId: string;
|
|
12
|
+
senderPublicKey: Uint8Array;
|
|
13
|
+
timestamp: number;
|
|
14
|
+
introductionBy?: string;
|
|
15
|
+
transactionEvent?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PermissionOptions {
|
|
19
|
+
customHandler?: (
|
|
20
|
+
senderId: string,
|
|
21
|
+
context: ContactContext,
|
|
22
|
+
) => boolean | Promise<boolean>;
|
|
23
|
+
transactionVerifier?: (
|
|
24
|
+
senderId: string,
|
|
25
|
+
event: string,
|
|
26
|
+
) => boolean | Promise<boolean>;
|
|
27
|
+
allowList?: Set<string>;
|
|
28
|
+
blockList?: Set<string>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ContactRequest {
|
|
32
|
+
id: string;
|
|
33
|
+
fromPeerId: string;
|
|
34
|
+
toPeerId: string;
|
|
35
|
+
timestamp: number;
|
|
36
|
+
status: 'pending' | 'accepted' | 'rejected';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface IntroductionRequest {
|
|
40
|
+
id: string;
|
|
41
|
+
fromPeerId: string;
|
|
42
|
+
toPeerId: string;
|
|
43
|
+
introducerPeerId: string;
|
|
44
|
+
timestamp: number;
|
|
45
|
+
status: 'pending' | 'introduced' | 'accepted' | 'rejected';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// --- Helpers ---
|
|
49
|
+
|
|
50
|
+
function generateId(): string {
|
|
51
|
+
const bytes = new Uint8Array(16);
|
|
52
|
+
crypto.getRandomValues(bytes);
|
|
53
|
+
return Array.from(bytes)
|
|
54
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
55
|
+
.join('');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// --- PermissionManager ---
|
|
59
|
+
|
|
60
|
+
export class PermissionManager {
|
|
61
|
+
private model: PermissionModel;
|
|
62
|
+
private readonly customHandler?: (
|
|
63
|
+
senderId: string,
|
|
64
|
+
context: ContactContext,
|
|
65
|
+
) => boolean | Promise<boolean>;
|
|
66
|
+
private readonly transactionVerifier?: (
|
|
67
|
+
senderId: string,
|
|
68
|
+
event: string,
|
|
69
|
+
) => boolean | Promise<boolean>;
|
|
70
|
+
|
|
71
|
+
private readonly contacts: Set<string> = new Set();
|
|
72
|
+
private readonly blocked: Set<string> = new Set();
|
|
73
|
+
private readonly allowed: Set<string> = new Set();
|
|
74
|
+
|
|
75
|
+
/** Peers who have sent us a contact request (mutual model). */
|
|
76
|
+
private readonly inboundRequests: Map<string, ContactRequest> = new Map();
|
|
77
|
+
/** Peers we have sent a contact request to (mutual model). */
|
|
78
|
+
private readonly outboundRequests: Map<string, ContactRequest> = new Map();
|
|
79
|
+
|
|
80
|
+
/** Introduction requests keyed by request id. */
|
|
81
|
+
private readonly introductionRequests: Map<string, IntroductionRequest> =
|
|
82
|
+
new Map();
|
|
83
|
+
|
|
84
|
+
/** Peers whose mutual contact has been confirmed (both sides accepted). */
|
|
85
|
+
private readonly mutualConfirmed: Set<string> = new Set();
|
|
86
|
+
|
|
87
|
+
/** Peers introduced to us via a trusted intermediary. */
|
|
88
|
+
private readonly introducedPeers: Set<string> = new Set();
|
|
89
|
+
|
|
90
|
+
constructor(model: PermissionModel, options?: PermissionOptions) {
|
|
91
|
+
this.model = model;
|
|
92
|
+
|
|
93
|
+
if (options?.customHandler) {
|
|
94
|
+
this.customHandler = options.customHandler;
|
|
95
|
+
}
|
|
96
|
+
if (options?.transactionVerifier) {
|
|
97
|
+
this.transactionVerifier = options.transactionVerifier;
|
|
98
|
+
}
|
|
99
|
+
if (options?.allowList) {
|
|
100
|
+
for (const id of options.allowList) {
|
|
101
|
+
this.allowed.add(id);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (options?.blockList) {
|
|
105
|
+
for (const id of options.blockList) {
|
|
106
|
+
this.blocked.add(id);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ------------------------------------------------------------------
|
|
112
|
+
// Model switching
|
|
113
|
+
// ------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
setModel(model: PermissionModel): void {
|
|
116
|
+
this.model = model;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
getModel(): PermissionModel {
|
|
120
|
+
return this.model;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ------------------------------------------------------------------
|
|
124
|
+
// Contact management
|
|
125
|
+
// ------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
addContact(peerId: string): void {
|
|
128
|
+
this.contacts.add(peerId);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
removeContact(peerId: string): void {
|
|
132
|
+
this.contacts.delete(peerId);
|
|
133
|
+
this.mutualConfirmed.delete(peerId);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
isContact(peerId: string): boolean {
|
|
137
|
+
return this.contacts.has(peerId);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
getContacts(): string[] {
|
|
141
|
+
return Array.from(this.contacts);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
loadContacts(contacts: string[]): void {
|
|
145
|
+
for (const id of contacts) this.contacts.add(id);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
blockPeer(peerId: string): void {
|
|
149
|
+
this.blocked.add(peerId);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
unblockPeer(peerId: string): void {
|
|
153
|
+
this.blocked.delete(peerId);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
isBlocked(peerId: string): boolean {
|
|
157
|
+
return this.blocked.has(peerId);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ------------------------------------------------------------------
|
|
161
|
+
// Permission checks
|
|
162
|
+
// ------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Main inbound permission check.
|
|
166
|
+
* Order: blockList -> allowList -> model-specific logic.
|
|
167
|
+
*/
|
|
168
|
+
async canReceiveFrom(
|
|
169
|
+
senderId: string,
|
|
170
|
+
context: ContactContext,
|
|
171
|
+
): Promise<boolean> {
|
|
172
|
+
// Always deny blocked peers.
|
|
173
|
+
if (this.blocked.has(senderId)) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Always allow explicitly allowed peers.
|
|
178
|
+
if (this.allowed.has(senderId)) {
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Existing contacts are always permitted regardless of model.
|
|
183
|
+
if (this.contacts.has(senderId)) {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
switch (this.model) {
|
|
188
|
+
case 'open':
|
|
189
|
+
return true;
|
|
190
|
+
|
|
191
|
+
case 'mutual':
|
|
192
|
+
return this.mutualConfirmed.has(senderId);
|
|
193
|
+
|
|
194
|
+
case 'introduction':
|
|
195
|
+
return this.introducedPeers.has(senderId) || this.checkIntroduction(senderId, context);
|
|
196
|
+
|
|
197
|
+
case 'transactional':
|
|
198
|
+
return this.checkTransaction(senderId, context);
|
|
199
|
+
|
|
200
|
+
case 'custom':
|
|
201
|
+
return this.checkCustom(senderId, context);
|
|
202
|
+
|
|
203
|
+
default:
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Outbound permission check — can we send to this recipient?
|
|
210
|
+
* Primarily relevant for the mutual model where both sides
|
|
211
|
+
* need to have each other as contacts.
|
|
212
|
+
*/
|
|
213
|
+
async canSendTo(recipientId: string): Promise<boolean> {
|
|
214
|
+
// Never send to blocked peers.
|
|
215
|
+
if (this.blocked.has(recipientId)) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Always allowed peers.
|
|
220
|
+
if (this.allowed.has(recipientId)) {
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
switch (this.model) {
|
|
225
|
+
case 'open':
|
|
226
|
+
return true;
|
|
227
|
+
|
|
228
|
+
case 'mutual':
|
|
229
|
+
// Can only send if mutual contact has been confirmed or they are
|
|
230
|
+
// already a known contact.
|
|
231
|
+
return this.contacts.has(recipientId) || this.mutualConfirmed.has(recipientId);
|
|
232
|
+
|
|
233
|
+
case 'introduction':
|
|
234
|
+
return (
|
|
235
|
+
this.contacts.has(recipientId) || this.introducedPeers.has(recipientId)
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
case 'transactional':
|
|
239
|
+
// For outbound in transactional model, allow if they are a contact.
|
|
240
|
+
return this.contacts.has(recipientId);
|
|
241
|
+
|
|
242
|
+
case 'custom':
|
|
243
|
+
// Custom model defers to the handler for inbound only;
|
|
244
|
+
// outbound is permitted to any non-blocked peer.
|
|
245
|
+
return true;
|
|
246
|
+
|
|
247
|
+
default:
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ------------------------------------------------------------------
|
|
253
|
+
// Mutual model logic
|
|
254
|
+
// ------------------------------------------------------------------
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Create a contact request to send to a peer (mutual model).
|
|
258
|
+
* The caller is responsible for transmitting the request over the wire.
|
|
259
|
+
*/
|
|
260
|
+
requestMutualContact(peerId: string): ContactRequest {
|
|
261
|
+
const request: ContactRequest = {
|
|
262
|
+
id: generateId(),
|
|
263
|
+
fromPeerId: '', // Will be filled by the caller with own peer ID
|
|
264
|
+
toPeerId: peerId,
|
|
265
|
+
timestamp: Date.now(),
|
|
266
|
+
status: 'pending',
|
|
267
|
+
};
|
|
268
|
+
this.outboundRequests.set(peerId, request);
|
|
269
|
+
return request;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Handle an inbound contact request from a peer.
|
|
274
|
+
* Returns true if the request is accepted (i.e. we already have an
|
|
275
|
+
* outbound request to that peer, achieving mutual consent), false
|
|
276
|
+
* if the request is stored as pending.
|
|
277
|
+
*/
|
|
278
|
+
handleContactRequest(request: ContactRequest): boolean {
|
|
279
|
+
if (this.blocked.has(request.fromPeerId)) {
|
|
280
|
+
request.status = 'rejected';
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
this.inboundRequests.set(request.fromPeerId, request);
|
|
285
|
+
|
|
286
|
+
// If we already sent a request to this peer, auto-confirm mutual contact.
|
|
287
|
+
if (this.outboundRequests.has(request.fromPeerId)) {
|
|
288
|
+
request.status = 'accepted';
|
|
289
|
+
const outbound = this.outboundRequests.get(request.fromPeerId)!;
|
|
290
|
+
outbound.status = 'accepted';
|
|
291
|
+
this.confirmMutualContact(request.fromPeerId);
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Explicitly confirm mutual contact with a peer.
|
|
300
|
+
* Adds the peer to contacts and the mutualConfirmed set.
|
|
301
|
+
*/
|
|
302
|
+
confirmMutualContact(peerId: string): void {
|
|
303
|
+
this.mutualConfirmed.add(peerId);
|
|
304
|
+
this.contacts.add(peerId);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ------------------------------------------------------------------
|
|
308
|
+
// Introduction model logic
|
|
309
|
+
// ------------------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Request an introduction to a target peer through a mutual contact.
|
|
313
|
+
* The caller is responsible for transmitting the request to the introducer.
|
|
314
|
+
*/
|
|
315
|
+
requestIntroduction(
|
|
316
|
+
targetPeerId: string,
|
|
317
|
+
introducerPeerId: string,
|
|
318
|
+
): IntroductionRequest {
|
|
319
|
+
const request: IntroductionRequest = {
|
|
320
|
+
id: generateId(),
|
|
321
|
+
fromPeerId: '', // Will be filled by the caller with own peer ID
|
|
322
|
+
toPeerId: targetPeerId,
|
|
323
|
+
introducerPeerId,
|
|
324
|
+
timestamp: Date.now(),
|
|
325
|
+
status: 'pending',
|
|
326
|
+
};
|
|
327
|
+
this.introductionRequests.set(request.id, request);
|
|
328
|
+
return request;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Handle an introduction request as the intermediary.
|
|
333
|
+
* Returns true if the introducer is a known contact of ours (trusted),
|
|
334
|
+
* false otherwise (the intermediary should only broker introductions
|
|
335
|
+
* between their own contacts).
|
|
336
|
+
*/
|
|
337
|
+
handleIntroductionRequest(request: IntroductionRequest): boolean {
|
|
338
|
+
if (this.blocked.has(request.fromPeerId)) {
|
|
339
|
+
request.status = 'rejected';
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// The intermediary should know both parties as contacts.
|
|
344
|
+
const knowsSender = this.contacts.has(request.fromPeerId);
|
|
345
|
+
const knowsTarget = this.contacts.has(request.toPeerId);
|
|
346
|
+
|
|
347
|
+
if (knowsSender && knowsTarget) {
|
|
348
|
+
request.status = 'introduced';
|
|
349
|
+
this.introductionRequests.set(request.id, request);
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
request.status = 'rejected';
|
|
354
|
+
this.introductionRequests.set(request.id, request);
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Confirm an introduction — called on the receiving end once the
|
|
360
|
+
* introduction has been brokered by the intermediary.
|
|
361
|
+
*/
|
|
362
|
+
confirmIntroduction(request: IntroductionRequest): void {
|
|
363
|
+
request.status = 'accepted';
|
|
364
|
+
this.introductionRequests.set(request.id, request);
|
|
365
|
+
this.introducedPeers.add(request.fromPeerId);
|
|
366
|
+
this.contacts.add(request.fromPeerId);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ------------------------------------------------------------------
|
|
370
|
+
// Private model-specific checks
|
|
371
|
+
// ------------------------------------------------------------------
|
|
372
|
+
|
|
373
|
+
private checkIntroduction(
|
|
374
|
+
senderId: string,
|
|
375
|
+
context: ContactContext,
|
|
376
|
+
): boolean {
|
|
377
|
+
// If the context carries a valid introduction reference, check it.
|
|
378
|
+
if (context.introductionBy) {
|
|
379
|
+
// The introducer must be one of our contacts.
|
|
380
|
+
return this.contacts.has(context.introductionBy);
|
|
381
|
+
}
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private async checkTransaction(
|
|
386
|
+
senderId: string,
|
|
387
|
+
context: ContactContext,
|
|
388
|
+
): Promise<boolean> {
|
|
389
|
+
if (!this.transactionVerifier || !context.transactionEvent) {
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
return this.transactionVerifier(senderId, context.transactionEvent);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private async checkCustom(
|
|
396
|
+
senderId: string,
|
|
397
|
+
context: ContactContext,
|
|
398
|
+
): Promise<boolean> {
|
|
399
|
+
if (!this.customHandler) {
|
|
400
|
+
// No custom handler configured — deny by default for safety.
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
return this.customHandler(senderId, context);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// MeshWhisper SDK — IndexedDB Storage Backend
|
|
3
|
+
// Implements StorageBackend for browsers and PWAs using the
|
|
4
|
+
// native IndexedDB API. No external dependencies.
|
|
5
|
+
// ============================================================
|
|
6
|
+
|
|
7
|
+
import type { StorageBackend } from './types.js';
|
|
8
|
+
|
|
9
|
+
const DB_NAME = 'meshwhisper';
|
|
10
|
+
const STORE_NAME = 'kv';
|
|
11
|
+
const DB_VERSION = 1;
|
|
12
|
+
|
|
13
|
+
function openDB(namespace: string): Promise<IDBDatabase> {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const req = indexedDB.open(`${DB_NAME}:${namespace}`, DB_VERSION);
|
|
16
|
+
req.onupgradeneeded = () => {
|
|
17
|
+
req.result.createObjectStore(STORE_NAME);
|
|
18
|
+
};
|
|
19
|
+
req.onsuccess = () => resolve(req.result);
|
|
20
|
+
req.onerror = () => reject(req.error);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function request<T>(req: IDBRequest<T>): Promise<T> {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
req.onsuccess = () => resolve(req.result);
|
|
27
|
+
req.onerror = () => reject(req.error);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* IndexedDB-backed storage for MeshWhisper. Use this in browsers and PWAs.
|
|
33
|
+
*
|
|
34
|
+
* ```ts
|
|
35
|
+
* import { IDBStorage } from '@meshwhisper/sdk/browser';
|
|
36
|
+
*
|
|
37
|
+
* const mw = await MeshWhisper.init({
|
|
38
|
+
* namespace: 'com.example.app',
|
|
39
|
+
* storage: new IDBStorage('com.example.app'),
|
|
40
|
+
* });
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* When `MeshWhisper.init()` is called in a browser without an explicit
|
|
44
|
+
* `storage` option, an `IDBStorage` is created automatically.
|
|
45
|
+
*/
|
|
46
|
+
export class IDBStorage implements StorageBackend {
|
|
47
|
+
private dbPromise: Promise<IDBDatabase> | null = null;
|
|
48
|
+
|
|
49
|
+
constructor(private readonly namespace: string = 'default') {}
|
|
50
|
+
|
|
51
|
+
private getDB(): Promise<IDBDatabase> {
|
|
52
|
+
if (!this.dbPromise) {
|
|
53
|
+
this.dbPromise = openDB(this.namespace);
|
|
54
|
+
}
|
|
55
|
+
return this.dbPromise;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async get(key: string): Promise<string | null> {
|
|
59
|
+
const db = await this.getDB();
|
|
60
|
+
const tx = db.transaction(STORE_NAME, 'readonly');
|
|
61
|
+
const val = await request<string | undefined>(tx.objectStore(STORE_NAME).get(key));
|
|
62
|
+
return val ?? null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async set(key: string, value: string): Promise<void> {
|
|
66
|
+
const db = await this.getDB();
|
|
67
|
+
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
68
|
+
await request(tx.objectStore(STORE_NAME).put(value, key));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async delete(key: string): Promise<void> {
|
|
72
|
+
const db = await this.getDB();
|
|
73
|
+
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
74
|
+
await request(tx.objectStore(STORE_NAME).delete(key));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async keys(prefix: string): Promise<string[]> {
|
|
78
|
+
const db = await this.getDB();
|
|
79
|
+
const tx = db.transaction(STORE_NAME, 'readonly');
|
|
80
|
+
const allKeys = await request<IDBValidKey[]>(tx.objectStore(STORE_NAME).getAllKeys());
|
|
81
|
+
return (allKeys as string[]).filter((k) => k.startsWith(prefix));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// MeshWhisper SDK — Node.js StorageBackend
|
|
3
|
+
// Filesystem-based persistence using one file per key.
|
|
4
|
+
// Files are written with mode 0600 (owner read/write only).
|
|
5
|
+
// ============================================================
|
|
6
|
+
|
|
7
|
+
import * as fs from 'node:fs';
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
import type { StorageBackend } from './types.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Node.js StorageBackend backed by the local filesystem.
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { MeshWhisper } from '@meshwhisper/sdk';
|
|
17
|
+
* import { NodeStorage } from '@meshwhisper/sdk/persistence/node';
|
|
18
|
+
*
|
|
19
|
+
* const mw = await MeshWhisper.init({
|
|
20
|
+
* namespace: 'com.example.myapp',
|
|
21
|
+
* storage: new NodeStorage('./data/meshwhisper'),
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* Data layout under `dataDir/`:
|
|
26
|
+
* identity — hex-encoded Ed25519 private key
|
|
27
|
+
* sessions/<id> — serialized RatchetState (JSON)
|
|
28
|
+
* peers/<id> — hex-encoded X25519 public key
|
|
29
|
+
* contacts — JSON array of peer ID strings
|
|
30
|
+
* messages/<id> — JSON array of StoredMessage
|
|
31
|
+
* seen_ids — JSON array of { id, ts } for deduplication
|
|
32
|
+
*/
|
|
33
|
+
export class NodeStorage implements StorageBackend {
|
|
34
|
+
private readonly root: string;
|
|
35
|
+
|
|
36
|
+
constructor(dataDir: string) {
|
|
37
|
+
this.root = path.resolve(dataDir);
|
|
38
|
+
fs.mkdirSync(this.root, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private resolve(key: string): string {
|
|
42
|
+
// Sanitize: only allow alphanumeric, underscore, hyphen, forward slash, dot
|
|
43
|
+
const safe = key.replace(/[^a-zA-Z0-9/_.-]/g, '_');
|
|
44
|
+
const abs = path.join(this.root, safe);
|
|
45
|
+
// Guard against path traversal
|
|
46
|
+
if (!abs.startsWith(this.root + path.sep) && abs !== this.root) {
|
|
47
|
+
throw new Error(`Storage key escapes data directory: ${key}`);
|
|
48
|
+
}
|
|
49
|
+
return abs;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async get(key: string): Promise<string | null> {
|
|
53
|
+
try {
|
|
54
|
+
return fs.readFileSync(this.resolve(key), 'utf-8');
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async set(key: string, value: string): Promise<void> {
|
|
61
|
+
const filePath = this.resolve(key);
|
|
62
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
63
|
+
// Write atomically: write to a temp file, then rename
|
|
64
|
+
const tmp = `${filePath}.tmp`;
|
|
65
|
+
fs.writeFileSync(tmp, value, { encoding: 'utf-8', mode: 0o600 });
|
|
66
|
+
fs.renameSync(tmp, filePath);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async delete(key: string): Promise<void> {
|
|
70
|
+
try {
|
|
71
|
+
fs.unlinkSync(this.resolve(key));
|
|
72
|
+
} catch {
|
|
73
|
+
// File doesn't exist — fine
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async keys(prefix: string): Promise<string[]> {
|
|
78
|
+
// Walk the directory corresponding to the prefix
|
|
79
|
+
const prefixPath = this.resolve(prefix);
|
|
80
|
+
// The prefix may itself be a directory (e.g. "sessions/") or a path fragment
|
|
81
|
+
const dir = prefix.endsWith('/') ? prefixPath : path.dirname(prefixPath);
|
|
82
|
+
const base = prefix.endsWith('/') ? '' : path.basename(prefixPath);
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
86
|
+
return entries
|
|
87
|
+
.filter((e) => e.isFile() && e.name.startsWith(base))
|
|
88
|
+
.map((e) => {
|
|
89
|
+
const rel = path.relative(this.root, path.join(dir, e.name));
|
|
90
|
+
return rel.replace(/\\/g, '/'); // normalise on Windows
|
|
91
|
+
});
|
|
92
|
+
} catch {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// MeshWhisper SDK — RatchetState serialization
|
|
3
|
+
// Converts between the in-memory RatchetState and a JSON string
|
|
4
|
+
// suitable for storage. All Uint8Array fields are hex-encoded.
|
|
5
|
+
// ============================================================
|
|
6
|
+
|
|
7
|
+
import type { RatchetState } from '../ratchet/index.js';
|
|
8
|
+
|
|
9
|
+
// ---- Helpers ----
|
|
10
|
+
|
|
11
|
+
function toHex(bytes: Uint8Array): string {
|
|
12
|
+
return Buffer.from(bytes).toString('hex');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function fromHex(hex: string): Uint8Array {
|
|
16
|
+
return new Uint8Array(Buffer.from(hex, 'hex'));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ---- Serialized shape ----
|
|
20
|
+
|
|
21
|
+
interface SerializedRatchetState {
|
|
22
|
+
v: 1;
|
|
23
|
+
dhSendingPriv: string;
|
|
24
|
+
dhSendingPub: string;
|
|
25
|
+
dhReceiving: string | null;
|
|
26
|
+
rootKey: string;
|
|
27
|
+
sendingChainKey: string | null;
|
|
28
|
+
receivingChainKey: string | null;
|
|
29
|
+
sendMessageNumber: number;
|
|
30
|
+
receiveMessageNumber: number;
|
|
31
|
+
previousSendingChainLength: number;
|
|
32
|
+
skippedMessageKeys: Record<string, string>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---- Public API ----
|
|
36
|
+
|
|
37
|
+
export function serializeRatchetState(state: RatchetState): string {
|
|
38
|
+
const obj: SerializedRatchetState = {
|
|
39
|
+
v: 1,
|
|
40
|
+
dhSendingPriv: toHex(state.dhSending.privateKey),
|
|
41
|
+
dhSendingPub: toHex(state.dhSending.publicKey),
|
|
42
|
+
dhReceiving: state.dhReceiving ? toHex(state.dhReceiving) : null,
|
|
43
|
+
rootKey: toHex(state.rootKey),
|
|
44
|
+
sendingChainKey: state.sendingChainKey ? toHex(state.sendingChainKey) : null,
|
|
45
|
+
receivingChainKey: state.receivingChainKey ? toHex(state.receivingChainKey) : null,
|
|
46
|
+
sendMessageNumber: state.sendMessageNumber,
|
|
47
|
+
receiveMessageNumber: state.receiveMessageNumber,
|
|
48
|
+
previousSendingChainLength: state.previousSendingChainLength,
|
|
49
|
+
skippedMessageKeys: Object.fromEntries(
|
|
50
|
+
[...state.skippedMessageKeys.entries()].map(([k, v]) => [k, toHex(v)]),
|
|
51
|
+
),
|
|
52
|
+
};
|
|
53
|
+
return JSON.stringify(obj);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function deserializeRatchetState(json: string): RatchetState {
|
|
57
|
+
const obj = JSON.parse(json) as SerializedRatchetState;
|
|
58
|
+
if (obj.v !== 1) throw new Error(`Unknown RatchetState version: ${obj.v}`);
|
|
59
|
+
return {
|
|
60
|
+
dhSending: {
|
|
61
|
+
privateKey: fromHex(obj.dhSendingPriv),
|
|
62
|
+
publicKey: fromHex(obj.dhSendingPub),
|
|
63
|
+
},
|
|
64
|
+
dhReceiving: obj.dhReceiving ? fromHex(obj.dhReceiving) : null,
|
|
65
|
+
rootKey: fromHex(obj.rootKey),
|
|
66
|
+
sendingChainKey: obj.sendingChainKey ? fromHex(obj.sendingChainKey) : null,
|
|
67
|
+
receivingChainKey: obj.receivingChainKey ? fromHex(obj.receivingChainKey) : null,
|
|
68
|
+
sendMessageNumber: obj.sendMessageNumber,
|
|
69
|
+
receiveMessageNumber: obj.receiveMessageNumber,
|
|
70
|
+
previousSendingChainLength: obj.previousSendingChainLength,
|
|
71
|
+
skippedMessageKeys: new Map(
|
|
72
|
+
Object.entries(obj.skippedMessageKeys).map(([k, v]) => [k, fromHex(v)]),
|
|
73
|
+
),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// MeshWhisper SDK — Persistence Types
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Minimal key-value storage interface. Implement this for your platform:
|
|
7
|
+
* - Node.js: NodeStorage (provided)
|
|
8
|
+
* - React Native: AsyncStorage or SQLCipher wrapper
|
|
9
|
+
* - Browser: IndexedDB wrapper
|
|
10
|
+
*
|
|
11
|
+
* Values are always JSON strings. Keys use forward-slash namespacing:
|
|
12
|
+
* identity, sessions/<peerId>, peers/<peerId>, contacts, messages/<peerId>, seen_ids
|
|
13
|
+
*/
|
|
14
|
+
export interface StorageBackend {
|
|
15
|
+
get(key: string): Promise<string | null>;
|
|
16
|
+
set(key: string, value: string): Promise<void>;
|
|
17
|
+
delete(key: string): Promise<void>;
|
|
18
|
+
/** Returns all stored keys that start with the given prefix. */
|
|
19
|
+
keys(prefix: string): Promise<string[]>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface StoredMessage {
|
|
23
|
+
id: string;
|
|
24
|
+
/** The peer ID of the other party in this conversation. */
|
|
25
|
+
conversationId: string;
|
|
26
|
+
senderId: string;
|
|
27
|
+
recipientId: string;
|
|
28
|
+
/** Message payload as a plain number array (JSON-serialisable Uint8Array). */
|
|
29
|
+
payload: number[];
|
|
30
|
+
timestamp: number;
|
|
31
|
+
direction: 'inbound' | 'outbound';
|
|
32
|
+
status: 'sending' | 'sent' | 'delivered' | 'read' | 'failed';
|
|
33
|
+
}
|