shogun-core 3.3.4 → 3.3.5

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.
@@ -1,350 +1,635 @@
1
1
  "use strict";
2
2
  /**
3
- * SHIP-06: Ephemeral P2P Messaging Implementation
3
+ * SHIP-06: Secure Vault Implementation
4
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
5
+ * Vault crittografato decentralizzato che dipende da SHIP-00 per l'identità.
10
6
  *
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
7
+ * Dipendenze:
8
+ * - SHIP-00 (Identity & Authentication) - per gestione utenti e chiavi
9
+ * - GunDB - per storage decentralizzato P2P
10
+ * - SEA - per crittografia AES-256-GCM
14
11
  *
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
12
+ * Ispirato a: https://github.com/draeder/gunsafe
19
13
  */
20
14
  Object.defineProperty(exports, "__esModule", { value: true });
21
15
  exports.SHIP_06 = void 0;
22
- const core_1 = require("../../src/core"); // ← Import diretto, NON da index!
23
16
  // ============================================================================
24
- // IMPLEMENTATION - STANDALONE VERSION (No ShogunCore dependency!)
17
+ // IMPLEMENTATION
25
18
  // ============================================================================
19
+ /**
20
+ * SHIP-06 Reference Implementation
21
+ *
22
+ * Questa implementazione dipende da ISHIP_00 per tutte le operazioni di identità.
23
+ * Si concentra esclusivamente sulla logica del vault crittografato.
24
+ */
26
25
  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;
26
+ /**
27
+ * Constructor
28
+ * @param identity ISHIP_00 instance for identity operations
29
+ * @param vaultNodeName Optional custom vault node name
30
+ */
31
+ constructor(identity, vaultNodeName) {
32
+ this.initialized = false;
35
33
  // 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;
34
+ this.vaultNode = null;
35
+ this.recordsNode = null;
36
+ this.metadataNode = null;
37
+ if (!identity.isLoggedIn()) {
38
+ throw new Error("User must be authenticated via SHIP-00 before using SHIP-06");
81
39
  }
40
+ this.identity = identity;
41
+ this.vaultNodeName = vaultNodeName || SHIP_06.DEFAULT_NODE_NAME;
42
+ console.log("✅ SHIP-06 initialized");
82
43
  }
44
+ /**
45
+ * Get identity provider
46
+ */
83
47
  getIdentity() {
84
- if (!this.identity) {
85
- throw new Error("No identity - SHIP-06 running in standalone mode");
86
- }
87
48
  return this.identity;
88
49
  }
89
- async connect() {
90
- if (this.connected)
50
+ // ========================================================================
51
+ // INITIALIZATION
52
+ // ========================================================================
53
+ /**
54
+ * Initialize vault
55
+ */
56
+ async initialize() {
57
+ if (this.initialized) {
58
+ console.warn("⚠️ Vault already initialized");
91
59
  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) {
60
+ }
61
+ try {
62
+ // Get Gun instance from identity
100
63
  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("");
64
+ if (!shogun || !shogun.db) {
65
+ throw new Error("Cannot access ShogunCore from identity");
66
+ }
67
+ const gun = shogun.db.gun;
68
+ if (!gun) {
69
+ throw new Error("Cannot access GunDB");
70
+ }
71
+ // Get user node
72
+ const userNode = gun.user();
73
+ if (!userNode || !userNode.is) {
74
+ throw new Error("User not authenticated in Gun");
75
+ }
76
+ // Setup vault nodes
77
+ this.vaultNode = userNode.get(this.vaultNodeName);
78
+ this.recordsNode = this.vaultNode.get("records");
79
+ this.metadataNode = this.vaultNode.get("metadata");
80
+ // Initialize metadata
81
+ const existingMetadata = await this.metadataNode.then();
82
+ if (!existingMetadata || !existingMetadata.version) {
83
+ await this.metadataNode.put({
84
+ version: SHIP_06.VAULT_VERSION,
85
+ created: Date.now().toString(),
86
+ recordCount: "0",
87
+ }).then();
88
+ console.log("📦 Vault metadata initialized");
115
89
  }
116
90
  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;
91
+ console.log("📦 Existing vault found");
123
92
  }
93
+ this.initialized = true;
94
+ console.log("✅ Vault initialized successfully");
124
95
  }
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);
96
+ catch (error) {
97
+ console.error("❌ Error initializing vault:", error);
98
+ throw error;
148
99
  }
149
- this.connected = false;
150
100
  }
151
- isConnected() {
152
- return this.connected;
153
- }
154
- getSwarmId() {
155
- return this.swarmId;
156
- }
157
- getAddress() {
158
- return this.myAddress;
101
+ /**
102
+ * Check if vault is initialized
103
+ */
104
+ isInitialized() {
105
+ return this.initialized;
159
106
  }
160
107
  // ========================================================================
161
- // PRESENCE
108
+ // CRUD OPERATIONS
162
109
  // ========================================================================
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
- });
110
+ /**
111
+ * Store encrypted record in vault
112
+ */
113
+ async put(name, data, metadata) {
114
+ if (!this.initialized) {
115
+ return { success: false, error: "Vault not initialized" };
116
+ }
117
+ if (!name || name.trim() === "") {
118
+ return { success: false, error: "Record name cannot be empty" };
119
+ }
120
+ try {
121
+ // Get crypto and key pair
122
+ const shogun = this.identity.getShogun();
123
+ const crypto = shogun?.db?.crypto;
124
+ const pair = this.identity.getKeyPair();
125
+ if (!crypto || !pair) {
126
+ return { success: false, error: "Cannot access encryption" };
127
+ }
128
+ // Encrypt data
129
+ const dataString = JSON.stringify(data);
130
+ const encryptedData = await crypto.encrypt(dataString, pair.epriv);
131
+ // Encrypt metadata if provided
132
+ let encryptedMetadata;
133
+ if (metadata) {
134
+ const metadataString = JSON.stringify(metadata);
135
+ encryptedMetadata = await crypto.encrypt(metadataString, pair.epriv);
186
136
  }
187
- }, 3000);
137
+ // Create encrypted record
138
+ const record = {
139
+ data: encryptedData,
140
+ created: Date.now().toString(),
141
+ updated: Date.now().toString(),
142
+ deleted: false,
143
+ metadata: encryptedMetadata,
144
+ };
145
+ // Store in vault
146
+ await this.recordsNode.get(name).put(record).then();
147
+ // Update vault metadata
148
+ await this.updateRecordCount();
149
+ console.log(`✅ Record stored: ${name}`);
150
+ return {
151
+ success: true,
152
+ recordName: name,
153
+ };
154
+ }
155
+ catch (error) {
156
+ console.error("❌ Error storing record:", error);
157
+ return {
158
+ success: false,
159
+ error: error.message,
160
+ };
161
+ }
188
162
  }
189
- listenForPeers() {
190
- if (this.config.debug) {
191
- console.log(`👂 Listening for peers on: ephemeral/${this.swarmId.substring(0, 20)}...`);
163
+ /**
164
+ * Retrieve and decrypt record from vault
165
+ */
166
+ async get(name, options) {
167
+ if (!this.initialized) {
168
+ console.error("❌ Vault not initialized");
169
+ return null;
192
170
  }
193
- this.presenceNode.map().on((data, address) => {
194
- if (!data || !address || address === "_" || address === this.myAddress) {
195
- return;
171
+ try {
172
+ // Retrieve encrypted record
173
+ const encryptedRecord = await this.recordsNode.get(name).then();
174
+ if (!encryptedRecord || !encryptedRecord.data) {
175
+ return null;
196
176
  }
197
- if (!data.pub || !data.epub || !data.timestamp) {
198
- return;
177
+ // Check if deleted (unless includeDeleted)
178
+ if (encryptedRecord.deleted && !options?.includeDeleted) {
179
+ return null;
199
180
  }
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));
181
+ // Get crypto and key pair
182
+ const shogun = this.identity.getShogun();
183
+ const crypto = shogun?.db?.crypto;
184
+ const pair = this.identity.getKeyPair();
185
+ if (!crypto || !pair) {
186
+ console.error("❌ Cannot access encryption");
187
+ return null;
188
+ }
189
+ // Decrypt data
190
+ const decryptedDataString = await crypto.decrypt(encryptedRecord.data, pair.epriv);
191
+ // Try to parse JSON, if it fails, use the string as-is
192
+ let decryptedData;
193
+ try {
194
+ decryptedData = JSON.parse(decryptedDataString);
195
+ }
196
+ catch (parseError) {
197
+ // If JSON.parse fails, the data is likely a plain string
198
+ // This can happen if the data was already a string value
199
+ decryptedData = decryptedDataString;
200
+ }
201
+ // Decrypt metadata if present
202
+ let decryptedMetadata;
203
+ if (encryptedRecord.metadata) {
204
+ try {
205
+ const metadataString = await crypto.decrypt(encryptedRecord.metadata, pair.epriv);
206
+ decryptedMetadata = JSON.parse(metadataString);
207
+ }
208
+ catch (error) {
209
+ // Metadata decryption failed, continue without it
210
+ console.warn("⚠️ Could not decrypt metadata");
212
211
  }
213
212
  }
213
+ // Create vault record
214
+ const vaultRecord = {
215
+ name,
216
+ data: decryptedData,
217
+ created: parseInt(encryptedRecord.created),
218
+ updated: parseInt(encryptedRecord.updated),
219
+ deleted: encryptedRecord.deleted,
220
+ metadata: decryptedMetadata,
221
+ };
222
+ return vaultRecord;
223
+ }
224
+ catch (error) {
225
+ console.error("❌ Error retrieving record:", error);
226
+ return null;
227
+ }
228
+ }
229
+ /**
230
+ * Delete record from vault (soft delete)
231
+ */
232
+ async delete(name) {
233
+ if (!this.initialized) {
234
+ return { success: false, error: "Vault not initialized" };
235
+ }
236
+ try {
237
+ if (name) {
238
+ // Delete specific record (soft delete)
239
+ const existingRecord = await this.recordsNode.get(name).then();
240
+ if (!existingRecord) {
241
+ return { success: false, error: `Record ${name} not found` };
242
+ }
243
+ // Mark as deleted
244
+ await this.recordsNode.get(name).get("deleted").put(true).then();
245
+ await this.recordsNode.get(name).get("updated").put(Date.now().toString()).then();
246
+ console.log(`🗑️ Record soft-deleted: ${name}`);
247
+ return {
248
+ success: true,
249
+ recordName: name,
250
+ };
251
+ }
214
252
  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;
253
+ // Delete all records (soft delete)
254
+ const allRecords = await this.list({ includeDeleted: false });
255
+ let deletedCount = 0;
256
+ for (const recordName of allRecords) {
257
+ const result = await this.delete(recordName);
258
+ if (result.success) {
259
+ deletedCount++;
260
+ }
221
261
  }
262
+ console.log(`🗑️ ${deletedCount} records soft-deleted`);
263
+ return {
264
+ success: true,
265
+ recordCount: deletedCount,
266
+ };
222
267
  }
223
- });
268
+ }
269
+ catch (error) {
270
+ console.error("❌ Error deleting record:", error);
271
+ return {
272
+ success: false,
273
+ error: error.message,
274
+ };
275
+ }
276
+ }
277
+ /**
278
+ * List all record names in vault
279
+ */
280
+ async list(options) {
281
+ if (!this.initialized) {
282
+ console.error("❌ Vault not initialized");
283
+ return [];
284
+ }
285
+ try {
286
+ return new Promise((resolve) => {
287
+ const recordNames = [];
288
+ this.recordsNode.map().once(async (record, key) => {
289
+ // Skip metadata
290
+ if (!record || typeof record !== "object" || key === "_") {
291
+ return;
292
+ }
293
+ // Skip deleted records (unless includeDeleted)
294
+ if (record.deleted && !options?.includeDeleted) {
295
+ return;
296
+ }
297
+ // Apply filters if provided
298
+ if (options?.filterByType || options?.filterByTag) {
299
+ try {
300
+ // Need to decrypt metadata to filter
301
+ const shogun = this.identity.getShogun();
302
+ const crypto = shogun?.db?.crypto;
303
+ const pair = this.identity.getKeyPair();
304
+ if (crypto && pair && record.metadata) {
305
+ const metadataString = await crypto.decrypt(record.metadata, pair.epriv);
306
+ const metadata = JSON.parse(metadataString);
307
+ // Filter by type
308
+ if (options.filterByType && metadata.type !== options.filterByType) {
309
+ return;
310
+ }
311
+ // Filter by tag
312
+ if (options.filterByTag) {
313
+ if (!metadata.tags || !metadata.tags.includes(options.filterByTag)) {
314
+ return;
315
+ }
316
+ }
317
+ }
318
+ }
319
+ catch (error) {
320
+ // Decryption failed, skip
321
+ return;
322
+ }
323
+ }
324
+ recordNames.push(key);
325
+ });
326
+ // Wait for Gun to return all records
327
+ setTimeout(() => {
328
+ // Sort if requested
329
+ if (options?.sortBy) {
330
+ // For now, just sort by name
331
+ recordNames.sort();
332
+ if (options.sortDesc) {
333
+ recordNames.reverse();
334
+ }
335
+ }
336
+ resolve(recordNames);
337
+ }, 1000);
338
+ });
339
+ }
340
+ catch (error) {
341
+ console.error("❌ Error listing records:", error);
342
+ return [];
343
+ }
344
+ }
345
+ /**
346
+ * Check if record exists
347
+ */
348
+ async exists(name) {
349
+ const record = await this.get(name);
350
+ return record !== null;
351
+ }
352
+ /**
353
+ * Update existing record
354
+ */
355
+ async update(name, data) {
356
+ if (!this.initialized) {
357
+ return { success: false, error: "Vault not initialized" };
358
+ }
359
+ try {
360
+ // Check if record exists
361
+ const existingRecord = await this.get(name);
362
+ if (!existingRecord) {
363
+ return { success: false, error: `Record ${name} not found` };
364
+ }
365
+ // Keep existing metadata
366
+ return await this.put(name, data, existingRecord.metadata);
367
+ }
368
+ catch (error) {
369
+ console.error("❌ Error updating record:", error);
370
+ return {
371
+ success: false,
372
+ error: error.message,
373
+ };
374
+ }
224
375
  }
225
376
  // ========================================================================
226
- // MESSAGING
377
+ // BACKUP & RESTORE
227
378
  // ========================================================================
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;
379
+ /**
380
+ * Export entire vault (encrypted)
381
+ */
382
+ async export(password, options) {
383
+ if (!this.initialized) {
384
+ throw new Error("Vault not initialized");
235
385
  }
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",
386
+ try {
387
+ // Get all records
388
+ const recordNames = await this.list({
389
+ includeDeleted: options?.includeDeleted || false,
390
+ filterByTag: options?.filterByTag,
391
+ filterByType: options?.filterByType,
392
+ });
393
+ const records = {};
394
+ for (const name of recordNames) {
395
+ const record = await this.get(name, {
396
+ includeDeleted: options?.includeDeleted || false,
249
397
  });
398
+ if (record) {
399
+ records[name] = record;
400
+ }
250
401
  }
251
- catch (error) {
252
- console.error(` ❌ Error sending to ${address.substring(0, 8)}:`, error);
402
+ // Create export data
403
+ const exportData = {
404
+ version: SHIP_06.VAULT_VERSION,
405
+ exportedAt: Date.now(),
406
+ exportedBy: this.identity.getCurrentUser()?.pub,
407
+ recordCount: Object.keys(records).length,
408
+ records,
409
+ };
410
+ // Serialize to JSON
411
+ const jsonString = options?.pretty
412
+ ? JSON.stringify(exportData, null, 2)
413
+ : JSON.stringify(exportData);
414
+ // Optionally encrypt with password
415
+ if (password) {
416
+ const crypto = this.identity.shogun?.db?.crypto;
417
+ if (!crypto) {
418
+ throw new Error("Cannot access crypto");
419
+ }
420
+ const encryptedJson = await crypto.encrypt(jsonString, password);
421
+ const base64 = Buffer.from(encryptedJson).toString("base64");
422
+ console.log(`✅ Vault exported (encrypted, ${base64.length} chars)`);
423
+ return base64;
253
424
  }
425
+ // Otherwise, return as base64
426
+ const base64 = Buffer.from(jsonString).toString("base64");
427
+ console.log(`✅ Vault exported (${base64.length} chars)`);
428
+ return base64;
429
+ }
430
+ catch (error) {
431
+ console.error("❌ Error exporting vault:", error);
432
+ throw error;
254
433
  }
255
434
  }
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);
435
+ /**
436
+ * Import vault from backup
437
+ */
438
+ async import(backupData, password, options) {
439
+ if (!this.initialized) {
440
+ return { success: false, error: "Vault not initialized" };
441
+ }
442
+ try {
443
+ // Decode base64
444
+ let jsonString = Buffer.from(backupData, "base64").toString("utf-8");
445
+ // Decrypt if password provided
446
+ if (password) {
447
+ const crypto = this.identity.shogun?.db?.crypto;
448
+ if (!crypto) {
449
+ return { success: false, error: "Cannot access crypto" };
295
450
  }
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));
451
+ jsonString = await crypto.decrypt(jsonString, password);
452
+ }
453
+ // Parse JSON
454
+ const importData = JSON.parse(jsonString);
455
+ // Validate version
456
+ if (importData.version !== SHIP_06.VAULT_VERSION) {
457
+ console.warn(`⚠️ Version mismatch: ${importData.version} vs ${SHIP_06.VAULT_VERSION}`);
312
458
  }
313
- catch (error) {
314
- if (this.config.debug) {
315
- console.error(` ❌ Error:`, error);
459
+ // Import records
460
+ let importedCount = 0;
461
+ let skippedCount = 0;
462
+ for (const [name, record] of Object.entries(importData.records)) {
463
+ const vaultRecord = record;
464
+ // Skip deleted records if requested
465
+ if (options?.skipDeleted && vaultRecord.deleted) {
466
+ skippedCount++;
467
+ continue;
468
+ }
469
+ // Check if exists (if merge mode)
470
+ const exists = await this.exists(name);
471
+ if (exists) {
472
+ if (options?.overwrite) {
473
+ // Overwrite existing
474
+ await this.put(name, vaultRecord.data, vaultRecord.metadata);
475
+ importedCount++;
476
+ }
477
+ else if (!options?.merge) {
478
+ // Skip if not merge and not overwrite
479
+ skippedCount++;
480
+ continue;
481
+ }
482
+ else {
483
+ // Merge mode: skip existing
484
+ skippedCount++;
485
+ continue;
486
+ }
487
+ }
488
+ else {
489
+ // Import new record
490
+ await this.put(name, vaultRecord.data, vaultRecord.metadata);
491
+ importedCount++;
316
492
  }
317
493
  }
318
- });
494
+ console.log(`✅ Vault imported: ${importedCount} records (${skippedCount} skipped)`);
495
+ return {
496
+ success: true,
497
+ recordCount: importedCount,
498
+ };
499
+ }
500
+ catch (error) {
501
+ console.error("❌ Error importing vault:", error);
502
+ return {
503
+ success: false,
504
+ error: error.message,
505
+ };
506
+ }
319
507
  }
320
508
  // ========================================================================
321
- // EVENT HANDLERS
509
+ // UTILITIES
322
510
  // ========================================================================
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);
511
+ /**
512
+ * Get vault statistics
513
+ */
514
+ async getStats() {
515
+ if (!this.initialized) {
516
+ throw new Error("Vault not initialized");
517
+ }
518
+ try {
519
+ const allRecords = await this.list({ includeDeleted: true });
520
+ const activeRecords = await this.list({ includeDeleted: false });
521
+ // Get metadata
522
+ const metadata = await this.metadataNode.then();
523
+ const stats = {
524
+ totalRecords: allRecords.length,
525
+ activeRecords: activeRecords.length,
526
+ deletedRecords: allRecords.length - activeRecords.length,
527
+ totalSize: 0, // TODO: Calculate actual size
528
+ created: metadata?.created ? parseInt(metadata.created) : Date.now(),
529
+ lastModified: Date.now(),
530
+ recordsByType: {},
531
+ };
532
+ // Count by type
533
+ for (const name of activeRecords) {
534
+ const record = await this.get(name);
535
+ if (record && record.metadata?.type) {
536
+ const type = record.metadata.type;
537
+ stats.recordsByType[type] = (stats.recordsByType[type] || 0) + 1;
538
+ }
539
+ }
540
+ return stats;
541
+ }
542
+ catch (error) {
543
+ console.error("❌ Error getting stats:", error);
544
+ throw error;
545
+ }
334
546
  }
335
- getPeers() {
336
- return Array.from(this.peers.keys());
547
+ /**
548
+ * Clear all records (soft delete all)
549
+ */
550
+ async clear() {
551
+ return await this.delete(); // Delete without name = delete all
337
552
  }
338
- getPeerInfo(address) {
339
- return this.peers.get(address) || null;
553
+ /**
554
+ * Compact vault (remove deleted records permanently)
555
+ */
556
+ async compact() {
557
+ if (!this.initialized) {
558
+ return { success: false, error: "Vault not initialized" };
559
+ }
560
+ try {
561
+ // Get all deleted records
562
+ const allRecords = await this.list({ includeDeleted: true });
563
+ let compactedCount = 0;
564
+ for (const name of allRecords) {
565
+ const record = await this.get(name, { includeDeleted: true });
566
+ if (record && record.deleted) {
567
+ // Permanently remove
568
+ await this.recordsNode.get(name).put(null).then();
569
+ compactedCount++;
570
+ }
571
+ }
572
+ // Update metadata
573
+ await this.updateRecordCount();
574
+ console.log(`✅ Vault compacted: ${compactedCount} records permanently removed`);
575
+ return {
576
+ success: true,
577
+ recordCount: compactedCount,
578
+ };
579
+ }
580
+ catch (error) {
581
+ console.error("❌ Error compacting vault:", error);
582
+ return {
583
+ success: false,
584
+ error: error.message,
585
+ };
586
+ }
340
587
  }
341
- async getEphemeralPair() {
342
- if (!this.myPair)
343
- throw new Error("No ephemeral pair");
344
- return this.myPair;
588
+ /**
589
+ * Search records by content
590
+ */
591
+ async search(query) {
592
+ if (!this.initialized) {
593
+ return [];
594
+ }
595
+ try {
596
+ const allRecords = await this.list({ includeDeleted: false });
597
+ const matches = [];
598
+ for (const name of allRecords) {
599
+ const record = await this.get(name);
600
+ if (record) {
601
+ // Search in data (converted to string)
602
+ const dataString = JSON.stringify(record.data).toLowerCase();
603
+ const queryLower = query.toLowerCase();
604
+ if (dataString.includes(queryLower) || name.toLowerCase().includes(queryLower)) {
605
+ matches.push(name);
606
+ }
607
+ }
608
+ }
609
+ return matches;
610
+ }
611
+ catch (error) {
612
+ console.error("❌ Error searching records:", error);
613
+ return [];
614
+ }
345
615
  }
346
- async setEphemeralPair(pair) {
347
- this.myPair = pair;
616
+ // ========================================================================
617
+ // PRIVATE HELPERS
618
+ // ========================================================================
619
+ /**
620
+ * Update record count in metadata
621
+ */
622
+ async updateRecordCount() {
623
+ try {
624
+ const activeRecords = await this.list({ includeDeleted: false });
625
+ await this.metadataNode.get("recordCount").put(activeRecords.length.toString()).then();
626
+ }
627
+ catch (error) {
628
+ console.error("❌ Error updating record count:", error);
629
+ }
348
630
  }
349
631
  }
350
632
  exports.SHIP_06 = SHIP_06;
633
+ // Constants
634
+ SHIP_06.VAULT_VERSION = "1.0.0";
635
+ SHIP_06.DEFAULT_NODE_NAME = "vault";