shogun-core 3.3.0 → 3.3.2
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/dist/browser/shogun-core.js +83301 -148719
- package/dist/browser/shogun-core.js.map +1 -1
- package/dist/ship/examples/ephemeral-cli.js +234 -0
- package/dist/ship/examples/identity-cli.js +503 -0
- package/dist/ship/examples/stealth-cli.js +433 -0
- package/dist/ship/examples/storage-cli.js +615 -0
- package/dist/ship/examples/vault-cli.js +444 -0
- package/dist/ship/implementation/SHIP_04.js +589 -0
- package/dist/ship/implementation/SHIP_05.js +1064 -0
- package/dist/ship/implementation/SHIP_06.js +350 -0
- package/dist/ship/implementation/SHIP_07.js +635 -0
- package/dist/ship/index.js +17 -0
- package/dist/ship/interfaces/ISHIP_04.js +62 -0
- package/dist/ship/interfaces/ISHIP_05.js +59 -0
- package/dist/ship/interfaces/ISHIP_06.js +144 -0
- package/dist/ship/interfaces/ISHIP_07.js +194 -0
- package/dist/src/index.js +1 -15
- package/dist/types/ship/examples/ephemeral-cli.d.ts +13 -0
- package/dist/types/ship/examples/identity-cli.d.ts +40 -0
- package/dist/types/ship/examples/stealth-cli.d.ts +31 -0
- package/dist/types/ship/examples/storage-cli.d.ts +48 -0
- package/dist/types/ship/examples/vault-cli.d.ts +13 -0
- package/dist/types/ship/implementation/SHIP_04.d.ts +76 -0
- package/dist/types/ship/implementation/SHIP_05.d.ts +70 -0
- package/dist/types/ship/implementation/SHIP_06.d.ts +66 -0
- package/dist/types/ship/implementation/SHIP_07.d.ts +101 -0
- package/dist/types/ship/index.d.ts +14 -0
- package/dist/types/ship/interfaces/ISHIP_04.d.ts +245 -0
- package/dist/types/ship/interfaces/ISHIP_05.d.ts +234 -0
- package/dist/types/ship/interfaces/ISHIP_06.d.ts +370 -0
- package/dist/types/ship/interfaces/ISHIP_07.d.ts +522 -0
- package/dist/types/src/index.d.ts +0 -10
- package/dist/types/src/types/shogun.d.ts +2 -0
- package/package.json +1 -1
- package/dist/browser/_e6ae.shogun-core.js +0 -14
- package/dist/browser/_e6ae.shogun-core.js.map +0 -1
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SHIP-06: Ephemeral P2P Messaging Implementation
|
|
4
|
+
*
|
|
5
|
+
* Two modes:
|
|
6
|
+
* 1. Standalone: new SHIP_06(gunPeers[], roomId) - NO authentication!
|
|
7
|
+
* - Uses ShogunCore internally with silent: true, disableAutoRecall: true
|
|
8
|
+
* - Zero logs, zero storage, pure relay communication
|
|
9
|
+
* - Room hashed with Web Crypto API SHA-256 for deterministic IDs
|
|
10
|
+
*
|
|
11
|
+
* 2. With Identity: new SHIP_06(ISHIP_00, roomId) - Authenticated sessions
|
|
12
|
+
* - Uses existing Gun instance from SHIP-00
|
|
13
|
+
* - All ShogunCore features available
|
|
14
|
+
*
|
|
15
|
+
* Architecture:
|
|
16
|
+
* - Gun Relay for P2P communication (no WebRTC complexity!)
|
|
17
|
+
* - SEA for ephemeral key generation and ECDH encryption
|
|
18
|
+
* - Pure relay mode: radisk: false, localStorage: false, multicast: false
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.SHIP_06 = void 0;
|
|
22
|
+
const core_1 = require("../../src/core"); // ← Import diretto, NON da index!
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// IMPLEMENTATION - STANDALONE VERSION (No ShogunCore dependency!)
|
|
25
|
+
// ============================================================================
|
|
26
|
+
class SHIP_06 {
|
|
27
|
+
// Constructor overload: with identity OR standalone
|
|
28
|
+
constructor(identityOrPeers, roomId, config) {
|
|
29
|
+
this.identity = null;
|
|
30
|
+
// State
|
|
31
|
+
this.connected = false;
|
|
32
|
+
this.swarmId = "";
|
|
33
|
+
this.myAddress = "";
|
|
34
|
+
this.myPair = null;
|
|
35
|
+
// Gun nodes
|
|
36
|
+
this.gun = null;
|
|
37
|
+
this.sea = null;
|
|
38
|
+
this.roomNode = null;
|
|
39
|
+
this.presenceNode = null;
|
|
40
|
+
this.messagesNode = null;
|
|
41
|
+
// Peers
|
|
42
|
+
this.peers = new Map();
|
|
43
|
+
// Event handlers
|
|
44
|
+
this.messageHandlers = [];
|
|
45
|
+
this.encryptedMessageHandlers = [];
|
|
46
|
+
this.peerSeenHandlers = [];
|
|
47
|
+
this.peerLeftHandlers = [];
|
|
48
|
+
// Heartbeat & cleanup
|
|
49
|
+
this.heartbeatInterval = null;
|
|
50
|
+
this.processedMessages = new Set();
|
|
51
|
+
this.roomId = roomId;
|
|
52
|
+
this.config = {
|
|
53
|
+
debug: config?.debug || false,
|
|
54
|
+
timeout: 30000,
|
|
55
|
+
};
|
|
56
|
+
if (Array.isArray(identityOrPeers)) {
|
|
57
|
+
// STANDALONE MODE - ShogunCore with silent mode
|
|
58
|
+
const shogunCore = new core_1.ShogunCore({
|
|
59
|
+
gunOptions: {
|
|
60
|
+
peers: identityOrPeers,
|
|
61
|
+
radisk: false,
|
|
62
|
+
localStorage: false,
|
|
63
|
+
multicast: false,
|
|
64
|
+
axe: false,
|
|
65
|
+
},
|
|
66
|
+
silent: true,
|
|
67
|
+
disableAutoRecall: true,
|
|
68
|
+
});
|
|
69
|
+
this.gun = shogunCore.db.gun;
|
|
70
|
+
this.sea = shogunCore.db.sea;
|
|
71
|
+
this.identity = null;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
// WITH IDENTITY MODE - use existing Gun from SHIP-00
|
|
75
|
+
if (!identityOrPeers.isLoggedIn()) {
|
|
76
|
+
throw new Error("User must be authenticated via SHIP-00");
|
|
77
|
+
}
|
|
78
|
+
this.identity = identityOrPeers;
|
|
79
|
+
const shogun = identityOrPeers.getShogun();
|
|
80
|
+
this.gun = shogun.db.gun;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
getIdentity() {
|
|
84
|
+
if (!this.identity) {
|
|
85
|
+
throw new Error("No identity - SHIP-06 running in standalone mode");
|
|
86
|
+
}
|
|
87
|
+
return this.identity;
|
|
88
|
+
}
|
|
89
|
+
async connect() {
|
|
90
|
+
if (this.connected)
|
|
91
|
+
return;
|
|
92
|
+
// Generate ephemeral pair
|
|
93
|
+
this.myPair = await this.sea.pair();
|
|
94
|
+
if (!this.myPair) {
|
|
95
|
+
throw new Error("Failed to generate SEA pair");
|
|
96
|
+
}
|
|
97
|
+
this.myAddress = this.myPair.pub.substring(0, 16);
|
|
98
|
+
// Hash room ID DETERMINISTICAMENTE - Simple SHA256 hash
|
|
99
|
+
if (this.identity) {
|
|
100
|
+
const shogun = this.identity.getShogun();
|
|
101
|
+
this.swarmId = await shogun.db.crypto.hashText(this.roomId);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// Standalone: use simple deterministic hash
|
|
105
|
+
// SEA.work with different calls produces different results, so we use a simple hash
|
|
106
|
+
const encoder = new TextEncoder();
|
|
107
|
+
const data = encoder.encode(this.roomId);
|
|
108
|
+
// Use Web Crypto API for deterministic SHA-256 hash
|
|
109
|
+
if (typeof crypto !== "undefined" && crypto.subtle) {
|
|
110
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
111
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
112
|
+
this.swarmId = hashArray
|
|
113
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
114
|
+
.join("");
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// Fallback: simple deterministic hash
|
|
118
|
+
let hash = "";
|
|
119
|
+
for (let i = 0; i < this.roomId.length; i++) {
|
|
120
|
+
hash += this.roomId.charCodeAt(i).toString(16);
|
|
121
|
+
}
|
|
122
|
+
this.swarmId = hash;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (this.config.debug) {
|
|
126
|
+
console.log(`🔑 Room ID: "${this.roomId}"`);
|
|
127
|
+
console.log(`🔒 Swarm ID (hashed): ${this.swarmId.substring(0, 32)}...`);
|
|
128
|
+
}
|
|
129
|
+
// Setup Gun nodes
|
|
130
|
+
this.roomNode = this.gun.get("ephemeral").get(this.swarmId);
|
|
131
|
+
this.presenceNode = this.roomNode.get("presence");
|
|
132
|
+
this.messagesNode = this.roomNode.get("messages");
|
|
133
|
+
// Announce presence
|
|
134
|
+
await this.announcePresence();
|
|
135
|
+
// Start listening
|
|
136
|
+
this.listenForPeers();
|
|
137
|
+
this.listenForMessages();
|
|
138
|
+
// Start heartbeat
|
|
139
|
+
this.startHeartbeat();
|
|
140
|
+
this.connected = true;
|
|
141
|
+
}
|
|
142
|
+
disconnect() {
|
|
143
|
+
if (this.heartbeatInterval) {
|
|
144
|
+
clearInterval(this.heartbeatInterval);
|
|
145
|
+
}
|
|
146
|
+
if (this.presenceNode && this.myAddress) {
|
|
147
|
+
this.presenceNode.get(this.myAddress).put(null);
|
|
148
|
+
}
|
|
149
|
+
this.connected = false;
|
|
150
|
+
}
|
|
151
|
+
isConnected() {
|
|
152
|
+
return this.connected;
|
|
153
|
+
}
|
|
154
|
+
getSwarmId() {
|
|
155
|
+
return this.swarmId;
|
|
156
|
+
}
|
|
157
|
+
getAddress() {
|
|
158
|
+
return this.myAddress;
|
|
159
|
+
}
|
|
160
|
+
// ========================================================================
|
|
161
|
+
// PRESENCE
|
|
162
|
+
// ========================================================================
|
|
163
|
+
async announcePresence() {
|
|
164
|
+
if (!this.myPair)
|
|
165
|
+
return;
|
|
166
|
+
const presenceData = {
|
|
167
|
+
address: this.myAddress,
|
|
168
|
+
pub: this.myPair.pub,
|
|
169
|
+
epub: this.myPair.epub,
|
|
170
|
+
timestamp: Date.now(),
|
|
171
|
+
};
|
|
172
|
+
if (this.config.debug) {
|
|
173
|
+
console.log(`📡 Announcing presence: ${this.myAddress}`);
|
|
174
|
+
}
|
|
175
|
+
await this.presenceNode.get(this.myAddress).put(presenceData);
|
|
176
|
+
}
|
|
177
|
+
startHeartbeat() {
|
|
178
|
+
this.heartbeatInterval = setInterval(() => {
|
|
179
|
+
if (this.myPair) {
|
|
180
|
+
this.presenceNode.get(this.myAddress).put({
|
|
181
|
+
address: this.myAddress,
|
|
182
|
+
pub: this.myPair.pub,
|
|
183
|
+
epub: this.myPair.epub,
|
|
184
|
+
timestamp: Date.now(),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}, 3000);
|
|
188
|
+
}
|
|
189
|
+
listenForPeers() {
|
|
190
|
+
if (this.config.debug) {
|
|
191
|
+
console.log(`👂 Listening for peers on: ephemeral/${this.swarmId.substring(0, 20)}...`);
|
|
192
|
+
}
|
|
193
|
+
this.presenceNode.map().on((data, address) => {
|
|
194
|
+
if (!data || !address || address === "_" || address === this.myAddress) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (!data.pub || !data.epub || !data.timestamp) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const age = Date.now() - (data.timestamp || 0);
|
|
201
|
+
const isOnline = age < 10000;
|
|
202
|
+
if (!this.peers.has(address)) {
|
|
203
|
+
this.peers.set(address, {
|
|
204
|
+
address,
|
|
205
|
+
pubKey: data.pub,
|
|
206
|
+
epub: data.epub,
|
|
207
|
+
pub: data.pub,
|
|
208
|
+
connectedAt: data.timestamp,
|
|
209
|
+
});
|
|
210
|
+
if (isOnline) {
|
|
211
|
+
this.peerSeenHandlers.forEach((h) => h(address));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
const peer = this.peers.get(address);
|
|
216
|
+
if (peer && data.timestamp > peer.connectedAt) {
|
|
217
|
+
peer.connectedAt = data.timestamp;
|
|
218
|
+
peer.pubKey = data.pub;
|
|
219
|
+
peer.epub = data.epub;
|
|
220
|
+
peer.pub = data.pub;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
// ========================================================================
|
|
226
|
+
// MESSAGING
|
|
227
|
+
// ========================================================================
|
|
228
|
+
async sendBroadcast(message) {
|
|
229
|
+
if (!this.connected || !this.myPair) {
|
|
230
|
+
throw new Error("Not connected");
|
|
231
|
+
}
|
|
232
|
+
if (this.peers.size === 0) {
|
|
233
|
+
console.warn("⚠️ No peers connected");
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
for (const [address, peer] of this.peers.entries()) {
|
|
237
|
+
try {
|
|
238
|
+
const secret = await this.sea.secret(peer.epub, this.myPair);
|
|
239
|
+
const encrypted = await this.sea.encrypt(message, secret);
|
|
240
|
+
const msgId = `${Date.now()}-${this.myAddress}-${Math.random().toString(36).substring(2, 9)}`;
|
|
241
|
+
await this.messagesNode.get(msgId).put({
|
|
242
|
+
from: this.myAddress,
|
|
243
|
+
fromPub: this.myPair.pub,
|
|
244
|
+
fromEpub: this.myPair.epub,
|
|
245
|
+
to: address,
|
|
246
|
+
content: encrypted,
|
|
247
|
+
timestamp: Date.now(),
|
|
248
|
+
type: "broadcast",
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
console.error(` ❌ Error sending to ${address.substring(0, 8)}:`, error);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
async sendDirect(peerAddress, message) {
|
|
257
|
+
const peer = this.peers.get(peerAddress);
|
|
258
|
+
if (!peer)
|
|
259
|
+
throw new Error(`Peer ${peerAddress} not found`);
|
|
260
|
+
if (!this.myPair)
|
|
261
|
+
throw new Error("No SEA pair");
|
|
262
|
+
const secret = await this.sea.secret(peer.epub, this.myPair);
|
|
263
|
+
const encrypted = await this.sea.encrypt(message, secret);
|
|
264
|
+
const msgId = `${Date.now()}-${this.myAddress}-${Math.random().toString(36).substring(2, 9)}`;
|
|
265
|
+
await this.messagesNode.get(msgId).put({
|
|
266
|
+
from: this.myAddress,
|
|
267
|
+
fromPub: this.myPair.pub,
|
|
268
|
+
fromEpub: this.myPair.epub,
|
|
269
|
+
to: peerAddress,
|
|
270
|
+
content: encrypted,
|
|
271
|
+
timestamp: Date.now(),
|
|
272
|
+
type: "direct",
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
listenForMessages() {
|
|
276
|
+
this.messagesNode.map().on(async (data, msgId) => {
|
|
277
|
+
if (!data || msgId === "_" || this.processedMessages.has(msgId))
|
|
278
|
+
return;
|
|
279
|
+
if (data.to !== this.myAddress || data.from === this.myAddress)
|
|
280
|
+
return;
|
|
281
|
+
this.processedMessages.add(msgId);
|
|
282
|
+
console.log(`\n📬 MESSAGE RECEIVED!`);
|
|
283
|
+
console.log(` From: ${data.from.substring(0, 12)}...`);
|
|
284
|
+
try {
|
|
285
|
+
let peer = this.peers.get(data.from);
|
|
286
|
+
if (!peer && data.fromPub && data.fromEpub) {
|
|
287
|
+
peer = {
|
|
288
|
+
address: data.from,
|
|
289
|
+
pubKey: data.fromPub,
|
|
290
|
+
epub: data.fromEpub,
|
|
291
|
+
pub: data.fromPub,
|
|
292
|
+
connectedAt: data.timestamp,
|
|
293
|
+
};
|
|
294
|
+
this.peers.set(data.from, peer);
|
|
295
|
+
}
|
|
296
|
+
if (!peer || !this.myPair)
|
|
297
|
+
return;
|
|
298
|
+
const senderEpub = data.fromEpub || peer.epub;
|
|
299
|
+
const secret = await this.sea.secret(senderEpub, this.myPair);
|
|
300
|
+
const decrypted = await this.sea.decrypt(data.content, secret);
|
|
301
|
+
if (!decrypted)
|
|
302
|
+
return;
|
|
303
|
+
console.log(` Content: "${decrypted}"`);
|
|
304
|
+
const ephemeralMsg = {
|
|
305
|
+
from: data.from,
|
|
306
|
+
fromPubKey: data.fromPub || peer.pubKey || "",
|
|
307
|
+
content: decrypted,
|
|
308
|
+
timestamp: data.timestamp,
|
|
309
|
+
type: data.type,
|
|
310
|
+
};
|
|
311
|
+
this.messageHandlers.forEach((h) => h(ephemeralMsg));
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
if (this.config.debug) {
|
|
315
|
+
console.error(` ❌ Error:`, error);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
// ========================================================================
|
|
321
|
+
// EVENT HANDLERS
|
|
322
|
+
// ========================================================================
|
|
323
|
+
onMessage(callback) {
|
|
324
|
+
this.messageHandlers.push(callback);
|
|
325
|
+
}
|
|
326
|
+
onPeerSeen(callback) {
|
|
327
|
+
this.peerSeenHandlers.push(callback);
|
|
328
|
+
}
|
|
329
|
+
onPeerLeft(callback) {
|
|
330
|
+
this.peerLeftHandlers.push(callback);
|
|
331
|
+
}
|
|
332
|
+
onEncryptedMessage(callback) {
|
|
333
|
+
this.encryptedMessageHandlers.push(callback);
|
|
334
|
+
}
|
|
335
|
+
getPeers() {
|
|
336
|
+
return Array.from(this.peers.keys());
|
|
337
|
+
}
|
|
338
|
+
getPeerInfo(address) {
|
|
339
|
+
return this.peers.get(address) || null;
|
|
340
|
+
}
|
|
341
|
+
async getEphemeralPair() {
|
|
342
|
+
if (!this.myPair)
|
|
343
|
+
throw new Error("No ephemeral pair");
|
|
344
|
+
return this.myPair;
|
|
345
|
+
}
|
|
346
|
+
async setEphemeralPair(pair) {
|
|
347
|
+
this.myPair = pair;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
exports.SHIP_06 = SHIP_06;
|