shogun-core 3.2.3 → 3.3.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 +12 -0
- package/dist/browser/shogun-core.js +108102 -43579
- package/dist/browser/shogun-core.js.map +1 -1
- package/dist/ship/examples/messenger-cli.js +171 -55
- package/dist/ship/examples/wallet-cli.js +767 -0
- package/dist/ship/implementation/SHIP_00.js +478 -0
- package/dist/ship/implementation/SHIP_01.js +275 -492
- package/dist/ship/implementation/SHIP_02.js +1366 -0
- package/dist/ship/implementation/SHIP_03.js +855 -0
- package/dist/ship/interfaces/ISHIP_00.js +135 -0
- package/dist/ship/interfaces/ISHIP_01.js +81 -24
- package/dist/ship/interfaces/ISHIP_02.js +57 -0
- package/dist/ship/interfaces/ISHIP_03.js +61 -0
- package/dist/src/gundb/db.js +55 -11
- package/dist/src/index.js +10 -2
- package/dist/src/managers/CoreInitializer.js +41 -13
- package/dist/src/storage/storage.js +22 -9
- package/dist/types/ship/examples/messenger-cli.d.ts +7 -1
- package/dist/types/ship/examples/wallet-cli.d.ts +131 -0
- package/dist/types/ship/implementation/SHIP_00.d.ts +113 -0
- package/dist/types/ship/implementation/SHIP_01.d.ts +44 -77
- package/dist/types/ship/implementation/SHIP_02.d.ts +297 -0
- package/dist/types/ship/implementation/SHIP_03.d.ts +127 -0
- package/dist/types/ship/interfaces/ISHIP_00.d.ts +410 -0
- package/dist/types/ship/interfaces/ISHIP_01.d.ts +157 -119
- package/dist/types/ship/interfaces/ISHIP_02.d.ts +470 -0
- package/dist/types/ship/interfaces/ISHIP_03.d.ts +295 -0
- package/dist/types/src/gundb/db.d.ts +10 -3
- package/dist/types/src/index.d.ts +7 -0
- package/dist/types/src/interfaces/shogun.d.ts +2 -0
- package/dist/types/src/storage/storage.d.ts +2 -1
- package/package.json +22 -9
|
@@ -1,351 +1,168 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* SHIP-01: Decentralized Encrypted Messaging Implementation
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* - Shogun Core per autenticazione (username/password)
|
|
7
|
-
* - GunDB per storage decentralizzato P2P
|
|
8
|
-
* - SEA (Security, Encryption, Authorization) per crittografia
|
|
5
|
+
* Messaggistica decentralizzata E2E che dipende da SHIP-00 per l'identità.
|
|
9
6
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* ✅ Offline-first
|
|
15
|
-
* ✅ End-to-end encryption
|
|
7
|
+
* Dipendenze:
|
|
8
|
+
* - SHIP-00 (Identity & Authentication) - per gestione utenti e chiavi
|
|
9
|
+
* - GunDB - per storage decentralizzato P2P
|
|
10
|
+
* - SEA - per crittografia ECDH + AES-GCM
|
|
16
11
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
12
|
+
* Vantaggi dell'architettura modulare:
|
|
13
|
+
* ✅ Separazione delle responsabilità (Identity vs Messaging)
|
|
14
|
+
* ✅ Riusabilità di SHIP-00 in altre applicazioni
|
|
15
|
+
* ✅ Testing più semplice e isolato
|
|
16
|
+
* ✅ Manutenibilità migliorata
|
|
22
17
|
*/
|
|
23
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
24
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
25
|
-
};
|
|
26
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
19
|
exports.SHIP_01 = void 0;
|
|
28
|
-
const core_1 = require("../../src/core");
|
|
29
20
|
const ethers_1 = require("ethers");
|
|
30
|
-
const derive_1 = __importDefault(require("../../src/gundb/derive"));
|
|
31
21
|
// ============================================================================
|
|
32
|
-
//
|
|
22
|
+
// IMPLEMENTATION
|
|
33
23
|
// ============================================================================
|
|
34
24
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
25
|
+
* SHIP-01 Reference Implementation
|
|
26
|
+
*
|
|
27
|
+
* Questa implementazione dipende da ISHIP_00 per tutte le operazioni di identità.
|
|
28
|
+
* Si concentra esclusivamente sulla logica di messaggistica.
|
|
38
29
|
*/
|
|
39
30
|
class SHIP_01 {
|
|
40
|
-
constructor(shogunConfig) {
|
|
41
|
-
// Inizializza Shogun Core
|
|
42
|
-
this.shogun = new core_1.ShogunCore(shogunConfig);
|
|
43
|
-
}
|
|
44
|
-
// ========================================================================
|
|
45
|
-
// 2. AUTENTICAZIONE: Username e Password
|
|
46
|
-
// ========================================================================
|
|
47
|
-
/**
|
|
48
|
-
* Registra un nuovo utente
|
|
49
|
-
*/
|
|
50
|
-
async signup(username, password) {
|
|
51
|
-
try {
|
|
52
|
-
// Registra con Shogun Core (crea SEA pair automaticamente)
|
|
53
|
-
const signupResult = await this.shogun.signUp(username, password);
|
|
54
|
-
if (!signupResult.success) {
|
|
55
|
-
return {
|
|
56
|
-
success: false,
|
|
57
|
-
error: signupResult.error || "Signup failed",
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
console.log("✅ Utente registrato");
|
|
61
|
-
console.log(` Username: ${username}`);
|
|
62
|
-
console.log(` GunDB Public Key: ${signupResult.pub}`);
|
|
63
|
-
// Opzionale: deriva address Ethereum dalla chiave GunDB
|
|
64
|
-
const derivedAddress = signupResult.pub
|
|
65
|
-
? await this.deriveEthereumAddress(signupResult.pub)
|
|
66
|
-
: undefined;
|
|
67
|
-
if (derivedAddress) {
|
|
68
|
-
console.log(` Derived Address: ${derivedAddress}`);
|
|
69
|
-
}
|
|
70
|
-
return {
|
|
71
|
-
success: true,
|
|
72
|
-
userPub: signupResult.pub,
|
|
73
|
-
derivedAddress,
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
catch (error) {
|
|
77
|
-
return {
|
|
78
|
-
success: false,
|
|
79
|
-
error: error.message,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Login con username e password
|
|
85
|
-
*/
|
|
86
|
-
async login(username, password) {
|
|
87
|
-
try {
|
|
88
|
-
// Login con Shogun Core
|
|
89
|
-
const loginResult = await this.shogun.login(username, password);
|
|
90
|
-
if (!loginResult.success) {
|
|
91
|
-
return {
|
|
92
|
-
success: false,
|
|
93
|
-
error: loginResult.error || "Login failed",
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
console.log("✅ Login effettuato");
|
|
97
|
-
console.log(` Username: ${username}`);
|
|
98
|
-
console.log(` GunDB Public Key: ${loginResult.userPub}`);
|
|
99
|
-
// Opzionale: deriva address Ethereum dalla chiave GunDB
|
|
100
|
-
const derivedAddress = loginResult.userPub
|
|
101
|
-
? await this.deriveEthereumAddress(loginResult.userPub)
|
|
102
|
-
: undefined;
|
|
103
|
-
if (derivedAddress) {
|
|
104
|
-
console.log(` Derived Address: ${derivedAddress}`);
|
|
105
|
-
}
|
|
106
|
-
return {
|
|
107
|
-
success: true,
|
|
108
|
-
userPub: loginResult.userPub,
|
|
109
|
-
derivedAddress,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
catch (error) {
|
|
113
|
-
return {
|
|
114
|
-
success: false,
|
|
115
|
-
error: error.message,
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
31
|
/**
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
* Autenticazione diretta usando un key pair esportato.
|
|
123
|
-
* Utile per:
|
|
124
|
-
* - Recupero account senza password
|
|
125
|
-
* - Portabilità tra dispositivi
|
|
126
|
-
* - Backup dell'identità
|
|
32
|
+
* Constructor
|
|
33
|
+
* @param identity ISHIP_00 instance for identity operations
|
|
127
34
|
*/
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// Autentica con GunDB usando il pair
|
|
132
|
-
const authResult = await new Promise((resolve) => {
|
|
133
|
-
this.shogun.db.gun.user().auth(seaPair, (ack) => {
|
|
134
|
-
if (ack.err) {
|
|
135
|
-
resolve({
|
|
136
|
-
success: false,
|
|
137
|
-
error: ack.err,
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
else {
|
|
141
|
-
resolve({
|
|
142
|
-
success: true,
|
|
143
|
-
userPub: seaPair.pub,
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
if (!authResult.success) {
|
|
149
|
-
console.log("❌ Login fallito:", authResult.error);
|
|
150
|
-
return authResult;
|
|
151
|
-
}
|
|
152
|
-
// Opzionale: deriva address Ethereum dalla chiave GunDB
|
|
153
|
-
const derivedAddress = await this.deriveEthereumAddress(seaPair.pub);
|
|
154
|
-
console.log("✅ Login effettuato");
|
|
155
|
-
console.log(` GunDB Public Key: ${seaPair.pub}`);
|
|
156
|
-
console.log(` Derived Address: ${derivedAddress}`);
|
|
157
|
-
return {
|
|
158
|
-
success: true,
|
|
159
|
-
userPub: authResult.userPub,
|
|
160
|
-
derivedAddress,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
catch (error) {
|
|
164
|
-
console.error("❌ Errore login con pair:", error);
|
|
165
|
-
return {
|
|
166
|
-
success: false,
|
|
167
|
-
error: error instanceof Error ? error.message : "Unknown error",
|
|
168
|
-
};
|
|
35
|
+
constructor(identity) {
|
|
36
|
+
if (!identity.isLoggedIn()) {
|
|
37
|
+
throw new Error("User must be authenticated via SHIP-00 before using SHIP-01");
|
|
169
38
|
}
|
|
39
|
+
this.identity = identity;
|
|
40
|
+
console.log("✅ SHIP-01 initialized with authenticated identity");
|
|
170
41
|
}
|
|
171
42
|
/**
|
|
172
|
-
*
|
|
173
|
-
*/
|
|
174
|
-
logout() {
|
|
175
|
-
this.shogun.logout();
|
|
176
|
-
console.log("👋 Logout effettuato");
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Verifica se l'utente è autenticato
|
|
180
|
-
*/
|
|
181
|
-
isLoggedIn() {
|
|
182
|
-
return this.shogun.isLoggedIn();
|
|
183
|
-
}
|
|
184
|
-
// ========================================================================
|
|
185
|
-
// 3. GESTIONE CHIAVI PUBBLICHE: Salva su GunDB
|
|
186
|
-
// ========================================================================
|
|
187
|
-
/**
|
|
188
|
-
* Salva la chiave pubblica dell'utente su GunDB
|
|
189
|
-
* Questo permette ad altri di trovare la tua chiave per criptare messaggi
|
|
43
|
+
* Get identity provider
|
|
190
44
|
*/
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (!this.isLoggedIn()) {
|
|
194
|
-
return { success: false, error: "Not logged in" };
|
|
195
|
-
}
|
|
196
|
-
// Ottieni il SEA pair dell'utente corrente
|
|
197
|
-
const currentUser = this.shogun.db.user;
|
|
198
|
-
if (!currentUser || !currentUser.is) {
|
|
199
|
-
return { success: false, error: "No user session" };
|
|
200
|
-
}
|
|
201
|
-
// Salva chiave pubblica sul nodo globale usando il userPub come chiave
|
|
202
|
-
// Questo permette ad altri di trovarla facilmente
|
|
203
|
-
const userPub = currentUser.is.pub;
|
|
204
|
-
await this.shogun.db.gun
|
|
205
|
-
.get(userPub)
|
|
206
|
-
.put({
|
|
207
|
-
pub: currentUser.is.pub,
|
|
208
|
-
epub: currentUser.is.epub,
|
|
209
|
-
algorithm: "ECDSA",
|
|
210
|
-
timestamp: Date.now().toString(),
|
|
211
|
-
})
|
|
212
|
-
.then();
|
|
213
|
-
console.log(`📝 Chiave pubblica pubblicata su GunDB`);
|
|
214
|
-
return { success: true };
|
|
215
|
-
}
|
|
216
|
-
catch (error) {
|
|
217
|
-
console.error("❌ Errore pubblicazione chiave:", error);
|
|
218
|
-
return { success: false, error: error.message };
|
|
219
|
-
}
|
|
45
|
+
getIdentity() {
|
|
46
|
+
return this.identity;
|
|
220
47
|
}
|
|
221
48
|
// ========================================================================
|
|
222
|
-
//
|
|
49
|
+
// MESSAGING - Send
|
|
223
50
|
// ========================================================================
|
|
224
51
|
/**
|
|
225
|
-
*
|
|
52
|
+
* Send encrypted message to a user
|
|
53
|
+
* Uses identity provider (SHIP-00) to get keys
|
|
226
54
|
*/
|
|
227
55
|
async sendMessage(recipientUsername, message) {
|
|
228
56
|
try {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const currentUser = this.shogun.db.user;
|
|
233
|
-
if (!currentUser || !currentUser.is) {
|
|
234
|
-
return { success: false, error: "No user session" };
|
|
57
|
+
// Verify authentication
|
|
58
|
+
if (!this.identity.isLoggedIn()) {
|
|
59
|
+
return { success: false, error: "Not authenticated" };
|
|
235
60
|
}
|
|
236
|
-
// 1.
|
|
237
|
-
const recipientKey = await this.
|
|
61
|
+
// 1. Get recipient's public key from SHIP-00
|
|
62
|
+
const recipientKey = await this.identity.getPublicKey(recipientUsername);
|
|
238
63
|
if (!recipientKey) {
|
|
239
64
|
return {
|
|
240
65
|
success: false,
|
|
241
66
|
error: `Recipient ${recipientUsername} has not published their public key`,
|
|
242
67
|
};
|
|
243
68
|
}
|
|
244
|
-
// 2.
|
|
245
|
-
const senderPair = this.
|
|
69
|
+
// 2. Get sender's key pair from SHIP-00
|
|
70
|
+
const senderPair = this.identity.getKeyPair();
|
|
246
71
|
if (!senderPair) {
|
|
247
|
-
return { success: false, error: "Cannot access
|
|
72
|
+
return { success: false, error: "Cannot access sender key pair" };
|
|
73
|
+
}
|
|
74
|
+
// 3. Get current user from SHIP-00
|
|
75
|
+
const currentUser = this.identity.getCurrentUser();
|
|
76
|
+
if (!currentUser) {
|
|
77
|
+
return { success: false, error: "No current user" };
|
|
78
|
+
}
|
|
79
|
+
// 4. Access GunDB through identity provider
|
|
80
|
+
const shogun = this.identity.getShogun();
|
|
81
|
+
if (!shogun || !shogun.db) {
|
|
82
|
+
return { success: false, error: "Cannot access ShogunCore" };
|
|
83
|
+
}
|
|
84
|
+
const gun = shogun.db.gun;
|
|
85
|
+
const crypto = shogun.db.crypto;
|
|
86
|
+
if (!gun || !crypto) {
|
|
87
|
+
return { success: false, error: "Cannot access GunDB or crypto" };
|
|
248
88
|
}
|
|
249
|
-
//
|
|
250
|
-
const encryptedMessage = await
|
|
251
|
-
|
|
252
|
-
);
|
|
253
|
-
// 4. Genera ID messaggio
|
|
89
|
+
// 5. Encrypt message using ECDH
|
|
90
|
+
const encryptedMessage = await crypto.encFor(message, senderPair, { epub: recipientKey.epub });
|
|
91
|
+
// 6. Generate message ID
|
|
254
92
|
const messageId = this.generateMessageId();
|
|
255
|
-
|
|
256
|
-
// 5. Salva messaggio crittografato su GunDB
|
|
93
|
+
// 7. Save encrypted message on GunDB
|
|
257
94
|
const messageData = {
|
|
258
|
-
from:
|
|
95
|
+
from: currentUser.pub,
|
|
259
96
|
to: recipientUsername,
|
|
260
|
-
content: encryptedMessage,
|
|
97
|
+
content: encryptedMessage,
|
|
261
98
|
timestamp: Date.now().toString(),
|
|
262
99
|
messageId: messageId,
|
|
263
100
|
};
|
|
264
|
-
|
|
265
|
-
await this.shogun.db.gun
|
|
101
|
+
await gun
|
|
266
102
|
.get(SHIP_01.NODES.MESSAGES)
|
|
267
103
|
.get(messageId)
|
|
268
104
|
.put(messageData)
|
|
269
105
|
.then();
|
|
270
|
-
console.log(`✅
|
|
106
|
+
console.log(`✅ Message sent: ${messageId}`);
|
|
271
107
|
return {
|
|
272
108
|
success: true,
|
|
273
109
|
messageId: messageId,
|
|
274
110
|
};
|
|
275
111
|
}
|
|
276
112
|
catch (error) {
|
|
113
|
+
console.error("❌ Error sending message:", error);
|
|
277
114
|
return {
|
|
278
115
|
success: false,
|
|
279
116
|
error: error.message,
|
|
280
117
|
};
|
|
281
118
|
}
|
|
282
119
|
}
|
|
283
|
-
/**
|
|
284
|
-
* Ottiene la chiave pubblica di un utente da GunDB
|
|
285
|
-
*/
|
|
286
|
-
async getRecipientPublicKey(username) {
|
|
287
|
-
try {
|
|
288
|
-
// Prima trova l'utente dal username
|
|
289
|
-
const userData = await this.shogun.db.getUserByAlias(username);
|
|
290
|
-
if (!userData || !userData.userPub) {
|
|
291
|
-
console.error(`❌ User ${username} not found`);
|
|
292
|
-
return null;
|
|
293
|
-
}
|
|
294
|
-
const userPub = userData.userPub;
|
|
295
|
-
// Le chiavi sono salvate direttamente sul nodo userPub
|
|
296
|
-
const publicKeyData = await this.shogun.db.gun.get(userPub).then();
|
|
297
|
-
if (publicKeyData && publicKeyData.epub && publicKeyData.pub) {
|
|
298
|
-
return {
|
|
299
|
-
pub: publicKeyData.pub,
|
|
300
|
-
epub: publicKeyData.epub,
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
return null;
|
|
304
|
-
}
|
|
305
|
-
catch (error) {
|
|
306
|
-
console.error("❌ Errore recupero chiave pubblica:", error);
|
|
307
|
-
return null;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
120
|
// ========================================================================
|
|
311
|
-
//
|
|
121
|
+
// MESSAGING - Listen
|
|
312
122
|
// ========================================================================
|
|
313
123
|
/**
|
|
314
|
-
*
|
|
124
|
+
* Listen for incoming encrypted messages
|
|
125
|
+
* Uses identity provider (SHIP-00) to decrypt messages
|
|
315
126
|
*/
|
|
316
127
|
async listenForMessages(onMessage) {
|
|
317
|
-
if (!this.isLoggedIn()) {
|
|
318
|
-
console.error("❌
|
|
128
|
+
if (!this.identity.isLoggedIn()) {
|
|
129
|
+
console.error("❌ Not authenticated");
|
|
319
130
|
return;
|
|
320
131
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
132
|
+
// Get current user from SHIP-00
|
|
133
|
+
const currentUser = this.identity.getCurrentUser();
|
|
134
|
+
if (!currentUser || !currentUser.alias) {
|
|
135
|
+
console.error("❌ No current user");
|
|
324
136
|
return;
|
|
325
137
|
}
|
|
326
|
-
const
|
|
327
|
-
//
|
|
328
|
-
const
|
|
329
|
-
|
|
138
|
+
const username = currentUser.alias;
|
|
139
|
+
// Access GunDB
|
|
140
|
+
const shogun = this.identity.getShogun();
|
|
141
|
+
const gun = shogun?.db?.gun;
|
|
142
|
+
if (!gun) {
|
|
143
|
+
console.error("❌ Cannot access GunDB");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
// Track received messages to avoid duplicates
|
|
330
147
|
const receivedMessages = new Set();
|
|
331
|
-
//
|
|
332
|
-
|
|
148
|
+
// Listen for messages in real-time
|
|
149
|
+
gun
|
|
333
150
|
.get(SHIP_01.NODES.MESSAGES)
|
|
334
151
|
.map()
|
|
335
152
|
.on(async (data, key) => {
|
|
336
|
-
//
|
|
153
|
+
// Filter messages for this user
|
|
337
154
|
if (data &&
|
|
338
155
|
data.to === username &&
|
|
339
156
|
data.from &&
|
|
340
157
|
data.content &&
|
|
341
158
|
data.messageId) {
|
|
342
|
-
//
|
|
159
|
+
// Avoid duplicates
|
|
343
160
|
if (receivedMessages.has(data.messageId)) {
|
|
344
161
|
return;
|
|
345
162
|
}
|
|
346
163
|
receivedMessages.add(data.messageId);
|
|
347
164
|
try {
|
|
348
|
-
//
|
|
165
|
+
// Decrypt message using SHIP-00
|
|
349
166
|
const decryptedContent = await this.decryptMessage(data.content, data.from);
|
|
350
167
|
onMessage({
|
|
351
168
|
from: data.from,
|
|
@@ -354,90 +171,55 @@ class SHIP_01 {
|
|
|
354
171
|
});
|
|
355
172
|
}
|
|
356
173
|
catch (error) {
|
|
357
|
-
console.error("❌
|
|
174
|
+
console.error("❌ Error decrypting message:", error);
|
|
358
175
|
}
|
|
359
176
|
}
|
|
360
177
|
});
|
|
361
|
-
console.log(`👂
|
|
362
|
-
}
|
|
363
|
-
/**
|
|
364
|
-
* Decripta un messaggio usando SEA.secret + SEA.decrypt (ECDH)
|
|
365
|
-
*/
|
|
366
|
-
async decryptMessage(encryptedContent, senderPub) {
|
|
367
|
-
// Ottieni il SEA pair completo del destinatario (noi)
|
|
368
|
-
const receiverPair = this.shogun.db.gun.user()?._?.sea;
|
|
369
|
-
if (!receiverPair) {
|
|
370
|
-
throw new Error("Cannot access SEA pair");
|
|
371
|
-
}
|
|
372
|
-
// Ottieni epub del mittente
|
|
373
|
-
const senderKey = await this.getPublicKeyByPub(senderPub);
|
|
374
|
-
if (!senderKey) {
|
|
375
|
-
throw new Error("Sender public key not found");
|
|
376
|
-
}
|
|
377
|
-
// Decripta usando SEA.secret + SEA.decrypt (ECDH)
|
|
378
|
-
const decrypted = await this.shogun.db.crypto.decFrom(encryptedContent, { epub: senderKey.epub }, // sender's public encryption key
|
|
379
|
-
receiverPair // receiver's pair completo
|
|
380
|
-
);
|
|
381
|
-
return decrypted;
|
|
382
|
-
}
|
|
383
|
-
/**
|
|
384
|
-
* Ottiene la chiave pubblica di un utente dalla sua pub key
|
|
385
|
-
*/
|
|
386
|
-
async getPublicKeyByPub(userPub) {
|
|
387
|
-
try {
|
|
388
|
-
// Le chiavi sono salvate direttamente sul nodo userPub
|
|
389
|
-
const publicKeyData = await this.shogun.db.gun.get(userPub).then();
|
|
390
|
-
if (publicKeyData && publicKeyData.epub && publicKeyData.pub) {
|
|
391
|
-
return {
|
|
392
|
-
pub: publicKeyData.pub,
|
|
393
|
-
epub: publicKeyData.epub,
|
|
394
|
-
};
|
|
395
|
-
}
|
|
396
|
-
return null;
|
|
397
|
-
}
|
|
398
|
-
catch (error) {
|
|
399
|
-
console.error("❌ Errore recupero chiave:", error);
|
|
400
|
-
return null;
|
|
401
|
-
}
|
|
178
|
+
console.log(`👂 Listening for messages to ${username}...`);
|
|
402
179
|
}
|
|
403
180
|
// ========================================================================
|
|
404
|
-
//
|
|
181
|
+
// MESSAGING - History
|
|
405
182
|
// ========================================================================
|
|
406
183
|
/**
|
|
407
|
-
*
|
|
184
|
+
* Get message history with a user
|
|
185
|
+
* Uses identity provider (SHIP-00) to decrypt messages
|
|
408
186
|
*/
|
|
409
187
|
async getMessageHistory(withUsername) {
|
|
410
|
-
if (!this.isLoggedIn()) {
|
|
411
|
-
console.error("❌
|
|
188
|
+
if (!this.identity.isLoggedIn()) {
|
|
189
|
+
console.error("❌ Not authenticated");
|
|
412
190
|
return [];
|
|
413
191
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
192
|
+
// Get current user from SHIP-00
|
|
193
|
+
const currentUser = this.identity.getCurrentUser();
|
|
194
|
+
if (!currentUser || !currentUser.alias) {
|
|
195
|
+
console.error("❌ No current user");
|
|
417
196
|
return [];
|
|
418
197
|
}
|
|
419
|
-
const
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
const otherUserData = await this.shogun.db.getUserByAlias(withUsername);
|
|
198
|
+
const username = currentUser.alias;
|
|
199
|
+
const userPub = currentUser.pub;
|
|
200
|
+
// Get other user's public key from SHIP-00
|
|
201
|
+
const otherUserData = await this.identity.getUserByAlias(withUsername);
|
|
424
202
|
const otherUserPub = otherUserData?.userPub;
|
|
425
|
-
//
|
|
203
|
+
// Access GunDB
|
|
204
|
+
const shogun = this.identity.getShogun();
|
|
205
|
+
const gun = shogun?.db?.gun;
|
|
206
|
+
if (!gun) {
|
|
207
|
+
console.error("❌ Cannot access GunDB");
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
// Get all messages and filter
|
|
426
211
|
return new Promise((resolve) => {
|
|
427
212
|
const messages = [];
|
|
428
|
-
|
|
429
|
-
this.shogun.db.gun
|
|
213
|
+
gun
|
|
430
214
|
.get(SHIP_01.NODES.MESSAGES)
|
|
431
215
|
.map()
|
|
432
216
|
.once(async (msgData, messageId) => {
|
|
433
|
-
// Skip metadata
|
|
217
|
+
// Skip metadata
|
|
434
218
|
if (!msgData || typeof msgData !== "object" || messageId === "_") {
|
|
435
219
|
return;
|
|
436
220
|
}
|
|
437
|
-
messageCount++;
|
|
438
221
|
try {
|
|
439
222
|
// Check if message is part of this conversation
|
|
440
|
-
// Fixed: Handle both username and userPub in matching
|
|
441
223
|
const isSentToTarget = msgData.from === userPub &&
|
|
442
224
|
(msgData.to === withUsername || msgData.to === otherUserPub);
|
|
443
225
|
const isReceivedFromTarget = (msgData.to === username || msgData.to === userPub) &&
|
|
@@ -457,7 +239,7 @@ class SHIP_01 {
|
|
|
457
239
|
// Silent error - message couldn't be decrypted
|
|
458
240
|
}
|
|
459
241
|
});
|
|
460
|
-
// Wait
|
|
242
|
+
// Wait for GunDB to return all messages, then resolve
|
|
461
243
|
setTimeout(() => {
|
|
462
244
|
const sorted = messages.sort((a, b) => a.timestamp - b.timestamp);
|
|
463
245
|
resolve(sorted);
|
|
@@ -465,186 +247,187 @@ class SHIP_01 {
|
|
|
465
247
|
});
|
|
466
248
|
}
|
|
467
249
|
// ========================================================================
|
|
468
|
-
//
|
|
250
|
+
// PRIVATE HELPERS
|
|
469
251
|
// ========================================================================
|
|
470
252
|
/**
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
253
|
+
* Decrypt a message using ECDH
|
|
254
|
+
* Uses identity provider (SHIP-00) to get keys
|
|
255
|
+
*/
|
|
256
|
+
async decryptMessage(encryptedContent, senderPub) {
|
|
257
|
+
// Get receiver's key pair from SHIP-00
|
|
258
|
+
const receiverPair = this.identity.getKeyPair();
|
|
259
|
+
if (!receiverPair) {
|
|
260
|
+
throw new Error("Cannot access receiver key pair");
|
|
261
|
+
}
|
|
262
|
+
// Get sender's public key
|
|
263
|
+
const senderKeyData = await this.getPublicKeyByPub(senderPub);
|
|
264
|
+
if (!senderKeyData) {
|
|
265
|
+
throw new Error("Sender public key not found");
|
|
266
|
+
}
|
|
267
|
+
// Access crypto
|
|
268
|
+
const shogun = this.identity.getShogun();
|
|
269
|
+
const crypto = shogun?.db?.crypto;
|
|
270
|
+
if (!crypto) {
|
|
271
|
+
throw new Error("Cannot access crypto");
|
|
272
|
+
}
|
|
273
|
+
// Decrypt using ECDH
|
|
274
|
+
const decrypted = await crypto.decFrom(encryptedContent, { epub: senderKeyData.epub }, receiverPair);
|
|
275
|
+
return decrypted;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Get public key by pub key
|
|
279
|
+
* Uses identity provider internally
|
|
478
280
|
*/
|
|
479
|
-
async
|
|
281
|
+
async getPublicKeyByPub(userPub) {
|
|
480
282
|
try {
|
|
481
|
-
//
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
|
|
283
|
+
// Access GunDB
|
|
284
|
+
const shogun = this.identity.getShogun();
|
|
285
|
+
const gun = shogun?.db?.gun;
|
|
286
|
+
if (!gun) {
|
|
287
|
+
return null;
|
|
485
288
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
}
|
|
493
|
-
return
|
|
289
|
+
const publicKeyData = await gun.get(userPub).then();
|
|
290
|
+
if (publicKeyData && publicKeyData.epub && publicKeyData.pub) {
|
|
291
|
+
return {
|
|
292
|
+
pub: publicKeyData.pub,
|
|
293
|
+
epub: publicKeyData.epub,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
return null;
|
|
494
297
|
}
|
|
495
298
|
catch (error) {
|
|
496
|
-
console.error("❌
|
|
497
|
-
|
|
498
|
-
const fallbackKey = publicKey || "unknown";
|
|
499
|
-
const hash = ethers_1.ethers.keccak256(ethers_1.ethers.toUtf8Bytes(fallbackKey));
|
|
500
|
-
return ethers_1.ethers.getAddress("0x" + hash.slice(-40));
|
|
299
|
+
console.error("❌ Error getting public key:", error);
|
|
300
|
+
return null;
|
|
501
301
|
}
|
|
502
302
|
}
|
|
503
303
|
/**
|
|
504
|
-
*
|
|
304
|
+
* Generate unique message ID
|
|
505
305
|
*/
|
|
506
306
|
generateMessageId() {
|
|
507
307
|
return ethers_1.ethers.hexlify(ethers_1.ethers.randomBytes(16));
|
|
508
308
|
}
|
|
309
|
+
// ========================================================================
|
|
310
|
+
// TOKEN-BASED MESSAGING (Channels/Groups)
|
|
311
|
+
// ========================================================================
|
|
312
|
+
/**
|
|
313
|
+
* Send message encrypted with shared token/password
|
|
314
|
+
* Useful for group chats, channels, broadcast messages
|
|
315
|
+
*/
|
|
316
|
+
async sendMessageWithToken(token, message, channel) {
|
|
317
|
+
try {
|
|
318
|
+
// Verify authentication
|
|
319
|
+
if (!this.identity.isLoggedIn()) {
|
|
320
|
+
return { success: false, error: "Not authenticated" };
|
|
321
|
+
}
|
|
322
|
+
// Get current user
|
|
323
|
+
const currentUser = this.identity.getCurrentUser();
|
|
324
|
+
if (!currentUser) {
|
|
325
|
+
return { success: false, error: "No current user" };
|
|
326
|
+
}
|
|
327
|
+
// Access crypto and GunDB
|
|
328
|
+
const shogun = this.identity.getShogun();
|
|
329
|
+
const crypto = shogun?.db?.crypto;
|
|
330
|
+
const gun = shogun?.db?.gun;
|
|
331
|
+
if (!crypto || !gun) {
|
|
332
|
+
return { success: false, error: "Cannot access crypto or GunDB" };
|
|
333
|
+
}
|
|
334
|
+
// Hash token for key derivation (more secure)
|
|
335
|
+
const hashedToken = await crypto.hashText(token);
|
|
336
|
+
// Encrypt message with hashed token
|
|
337
|
+
const encryptedMessage = await crypto.encrypt(message, hashedToken);
|
|
338
|
+
// Generate message ID
|
|
339
|
+
const messageId = this.generateMessageId();
|
|
340
|
+
// Save encrypted message
|
|
341
|
+
const messageData = {
|
|
342
|
+
from: currentUser.pub,
|
|
343
|
+
content: encryptedMessage,
|
|
344
|
+
channel: channel || "default",
|
|
345
|
+
timestamp: Date.now().toString(),
|
|
346
|
+
messageId: messageId,
|
|
347
|
+
type: "token", // Mark as token-encrypted
|
|
348
|
+
};
|
|
349
|
+
await gun
|
|
350
|
+
.get(SHIP_01.NODES.TOKEN_MESSAGES)
|
|
351
|
+
.get(messageId)
|
|
352
|
+
.put(messageData)
|
|
353
|
+
.then();
|
|
354
|
+
console.log(`✅ Token message sent: ${messageId} (channel: ${channel || "default"})`);
|
|
355
|
+
return {
|
|
356
|
+
success: true,
|
|
357
|
+
messageId: messageId,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
console.error("❌ Error sending token message:", error);
|
|
362
|
+
return {
|
|
363
|
+
success: false,
|
|
364
|
+
error: error.message,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Listen for token-encrypted messages
|
|
370
|
+
* Automatically decrypts with provided token
|
|
371
|
+
*/
|
|
372
|
+
async listenForTokenMessages(token, onMessage, channel) {
|
|
373
|
+
if (!this.identity.isLoggedIn()) {
|
|
374
|
+
console.error("❌ Not authenticated");
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
// Access GunDB and crypto
|
|
378
|
+
const shogun = this.identity.getShogun();
|
|
379
|
+
const gun = shogun?.db?.gun;
|
|
380
|
+
const crypto = shogun?.db?.crypto;
|
|
381
|
+
if (!gun || !crypto) {
|
|
382
|
+
console.error("❌ Cannot access GunDB or crypto");
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
// Hash token for decryption
|
|
386
|
+
const hashedToken = await crypto.hashText(token);
|
|
387
|
+
// Track received messages to avoid duplicates
|
|
388
|
+
const receivedMessages = new Set();
|
|
389
|
+
// Listen for token messages in real-time
|
|
390
|
+
gun
|
|
391
|
+
.get(SHIP_01.NODES.TOKEN_MESSAGES)
|
|
392
|
+
.map()
|
|
393
|
+
.on(async (data, key) => {
|
|
394
|
+
// Filter by channel if specified
|
|
395
|
+
if (channel && data?.channel !== channel) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
// Validate data
|
|
399
|
+
if (data &&
|
|
400
|
+
data.type === "token" &&
|
|
401
|
+
data.from &&
|
|
402
|
+
data.content &&
|
|
403
|
+
data.messageId) {
|
|
404
|
+
// Avoid duplicates
|
|
405
|
+
if (receivedMessages.has(data.messageId)) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
receivedMessages.add(data.messageId);
|
|
409
|
+
try {
|
|
410
|
+
// Decrypt message with hashed token
|
|
411
|
+
const decryptedContent = await crypto.decrypt(data.content, hashedToken);
|
|
412
|
+
onMessage({
|
|
413
|
+
from: data.from,
|
|
414
|
+
content: decryptedContent,
|
|
415
|
+
channel: data.channel,
|
|
416
|
+
timestamp: parseInt(data.timestamp),
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
// Silently skip messages that can't be decrypted
|
|
421
|
+
// (wrong token or corrupted data)
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
console.log(`👂 Listening for token messages (channel: ${channel || "all"})...`);
|
|
426
|
+
}
|
|
509
427
|
}
|
|
510
428
|
exports.SHIP_01 = SHIP_01;
|
|
511
|
-
// GunDB Node Names
|
|
429
|
+
// GunDB Node Names for messaging
|
|
512
430
|
SHIP_01.NODES = {
|
|
513
431
|
MESSAGES: "messages",
|
|
514
|
-
|
|
515
|
-
PUBLIC_KEYS: "publicKeys",
|
|
432
|
+
TOKEN_MESSAGES: "token_messages", // Token-encrypted messages (channels/groups)
|
|
516
433
|
};
|
|
517
|
-
async function testAliceAndBob() {
|
|
518
|
-
console.log("🧪 TEST: Alice e Bob - Conversazione Crittografata E2E\n");
|
|
519
|
-
console.log("=".repeat(60));
|
|
520
|
-
// Setup Alice
|
|
521
|
-
console.log("\n👩 ALICE - Setup");
|
|
522
|
-
console.log("-".repeat(60));
|
|
523
|
-
const alice = new SHIP_01({
|
|
524
|
-
gunOptions: {
|
|
525
|
-
peers: ["https://peer.wallie.io/gun"],
|
|
526
|
-
radisk: true,
|
|
527
|
-
},
|
|
528
|
-
});
|
|
529
|
-
// Alice signup/login
|
|
530
|
-
await alice.signup("alice", "password123");
|
|
531
|
-
const aliceLogin = await alice.login("alice", "password123");
|
|
532
|
-
if (!aliceLogin.success) {
|
|
533
|
-
console.error("❌ Alice login failed");
|
|
534
|
-
return;
|
|
535
|
-
}
|
|
536
|
-
await alice.publishPublicKey();
|
|
537
|
-
// Aspetta che la chiave sia sincronizzata
|
|
538
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
539
|
-
// Setup Bob
|
|
540
|
-
console.log("\n👨 BOB - Setup");
|
|
541
|
-
console.log("-".repeat(60));
|
|
542
|
-
const bob = new SHIP_01({
|
|
543
|
-
gunOptions: {
|
|
544
|
-
peers: ["https://peer.wallie.io/gun"],
|
|
545
|
-
radisk: true,
|
|
546
|
-
},
|
|
547
|
-
});
|
|
548
|
-
// Bob signup/login
|
|
549
|
-
await bob.signup("bob", "password456");
|
|
550
|
-
const bobLogin = await bob.login("bob", "password456");
|
|
551
|
-
if (!bobLogin.success) {
|
|
552
|
-
console.error("❌ Bob login failed");
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
await bob.publishPublicKey();
|
|
556
|
-
// Aspetta che la chiave sia sincronizzata
|
|
557
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
558
|
-
console.log("\n" + "=".repeat(60));
|
|
559
|
-
console.log("💬 CONVERSAZIONE");
|
|
560
|
-
console.log("=".repeat(60));
|
|
561
|
-
// Alice e Bob ascoltano messaggi
|
|
562
|
-
let aliceMessages = [];
|
|
563
|
-
let bobMessages = [];
|
|
564
|
-
await alice.listenForMessages((msg) => {
|
|
565
|
-
aliceMessages.push(msg);
|
|
566
|
-
console.log(`\n[Alice riceve] 📨`);
|
|
567
|
-
console.log(` Da: ${msg.from.substring(0, 10)}...`);
|
|
568
|
-
console.log(` Messaggio: "${msg.content}"`);
|
|
569
|
-
console.log(` Timestamp: ${new Date(msg.timestamp).toLocaleTimeString()}`);
|
|
570
|
-
});
|
|
571
|
-
await bob.listenForMessages((msg) => {
|
|
572
|
-
bobMessages.push(msg);
|
|
573
|
-
console.log(`\n[Bob riceve] 📨`);
|
|
574
|
-
console.log(` Da: ${msg.from.substring(0, 10)}...`);
|
|
575
|
-
console.log(` Messaggio: "${msg.content}"`);
|
|
576
|
-
console.log(` Timestamp: ${new Date(msg.timestamp).toLocaleTimeString()}`);
|
|
577
|
-
});
|
|
578
|
-
// Aspetta che i listener siano pronti
|
|
579
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
580
|
-
// Alice invia messaggio a Bob
|
|
581
|
-
console.log("\n[Alice invia] 📤");
|
|
582
|
-
const msg1 = await alice.sendMessage("bob", "Ciao Bob! Come stai? 👋");
|
|
583
|
-
console.log(` ✅ Inviato: "${msg1.messageId}"`);
|
|
584
|
-
// Aspetta che Bob riceva
|
|
585
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
586
|
-
// Bob risponde ad Alice
|
|
587
|
-
console.log("\n[Bob invia] 📤");
|
|
588
|
-
const msg2 = await bob.sendMessage("alice", "Ciao Alice! Tutto bene, grazie! Tu? 😊");
|
|
589
|
-
console.log(` ✅ Inviato: "${msg2.messageId}"`);
|
|
590
|
-
// Aspetta che Alice riceva
|
|
591
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
592
|
-
// Alice risponde
|
|
593
|
-
console.log("\n[Alice invia] 📤");
|
|
594
|
-
const msg3 = await alice.sendMessage("bob", "Anch'io benissimo! Questo sistema di messaggistica è fantastico! 🚀");
|
|
595
|
-
console.log(` ✅ Inviato: "${msg3.messageId}"`);
|
|
596
|
-
// Aspetta che Bob riceva
|
|
597
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
598
|
-
// Bob conclude
|
|
599
|
-
console.log("\n[Bob invia] 📤");
|
|
600
|
-
const msg4 = await bob.sendMessage("alice", "Vero! Zero costi, decentralizzato e veloce! 💪");
|
|
601
|
-
console.log(` ✅ Inviato: "${msg4.messageId}"`);
|
|
602
|
-
// Aspetta finale
|
|
603
|
-
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
604
|
-
// Mostra statistiche
|
|
605
|
-
console.log("\n" + "=".repeat(60));
|
|
606
|
-
console.log("📊 STATISTICHE FINALI");
|
|
607
|
-
console.log("=".repeat(60));
|
|
608
|
-
console.log(`\n👩 Alice:`);
|
|
609
|
-
console.log(` - Messaggi inviati: 2`);
|
|
610
|
-
console.log(` - Messaggi ricevuti: ${aliceMessages.length}`);
|
|
611
|
-
console.log(` - Public Key: ${aliceLogin.userPub?.substring(0, 20)}...`);
|
|
612
|
-
console.log(` - Derived Address: ${aliceLogin.derivedAddress}`);
|
|
613
|
-
console.log(`\n👨 Bob:`);
|
|
614
|
-
console.log(` - Messaggi inviati: 2`);
|
|
615
|
-
console.log(` - Messaggi ricevuti: ${bobMessages.length}`);
|
|
616
|
-
console.log(` - Public Key: ${bobLogin.userPub?.substring(0, 20)}...`);
|
|
617
|
-
console.log(` - Derived Address: ${bobLogin.derivedAddress}`);
|
|
618
|
-
// Recupera storico
|
|
619
|
-
console.log("\n" + "=".repeat(60));
|
|
620
|
-
console.log("📚 STORICO CONVERSAZIONE");
|
|
621
|
-
console.log("=".repeat(60));
|
|
622
|
-
const aliceHistory = await alice.getMessageHistory("bob");
|
|
623
|
-
console.log(`\n👩 Storico di Alice con Bob (${aliceHistory.length} messaggi):`);
|
|
624
|
-
aliceHistory.forEach((msg, i) => {
|
|
625
|
-
const sender = msg.from === aliceLogin.userPub ? "Alice" : "Bob";
|
|
626
|
-
console.log(` ${i + 1}. [${sender}]: ${msg.content}`);
|
|
627
|
-
});
|
|
628
|
-
const bobHistory = await bob.getMessageHistory("alice");
|
|
629
|
-
console.log(`\n👨 Storico di Bob con Alice (${bobHistory.length} messaggi):`);
|
|
630
|
-
bobHistory.forEach((msg, i) => {
|
|
631
|
-
const sender = msg.from === bobLogin.userPub ? "Bob" : "Alice";
|
|
632
|
-
console.log(` ${i + 1}. [${sender}]: ${msg.content}`);
|
|
633
|
-
});
|
|
634
|
-
// Riepilogo finale
|
|
635
|
-
console.log("\n" + "=".repeat(60));
|
|
636
|
-
console.log("✅ TEST COMPLETATO CON SUCCESSO!");
|
|
637
|
-
console.log("=".repeat(60));
|
|
638
|
-
console.log("\n📝 Riepilogo:");
|
|
639
|
-
console.log(" ✅ 2 utenti registrati (Alice, Bob)");
|
|
640
|
-
console.log(" ✅ 4 messaggi scambiati");
|
|
641
|
-
console.log(" ✅ Messaggi ricevuti in real-time");
|
|
642
|
-
console.log(" ✅ Storico recuperato correttamente");
|
|
643
|
-
console.log(" ✅ Storage: GunDB P2P (gratis!)");
|
|
644
|
-
console.log(" ✅ Costi: $0.00");
|
|
645
|
-
console.log("\n🎉 Sistema di messaggistica decentralizzato funzionante!\n");
|
|
646
|
-
}
|
|
647
|
-
// Esegui esempio o test
|
|
648
|
-
if (require.main === module) {
|
|
649
|
-
testAliceAndBob().catch(console.error); // Test completo Alice & Bob
|
|
650
|
-
}
|