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.
- package/dist/browser/defaultVendors-node_modules_shogun-ipfs_node_modules_axios_index_js.shogun-core.js +4073 -2
- package/dist/browser/defaultVendors-node_modules_shogun-ipfs_node_modules_axios_index_js.shogun-core.js.map +1 -0
- package/dist/browser/shogun-core.js +597 -343
- package/dist/browser/shogun-core.js.map +1 -1
- package/dist/ship/examples/identity-cli.js +12 -4
- package/dist/ship/examples/vault-cli.js +2 -2
- package/dist/ship/implementation/SHIP_06.js +574 -289
- package/dist/ship/interfaces/ISHIP_06.js +135 -85
- package/dist/src/gundb/db.js +7 -49
- package/dist/types/ship/implementation/SHIP_06.d.ts +90 -55
- package/dist/types/ship/interfaces/ISHIP_06.d.ts +383 -231
- package/package.json +2 -8
- package/dist/ship/examples/ephemeral-cli.js +0 -234
- package/dist/ship/implementation/SHIP_07.js +0 -635
- package/dist/ship/interfaces/ISHIP_07.js +0 -194
- package/dist/types/ship/examples/ephemeral-cli.d.ts +0 -13
- package/dist/types/ship/implementation/SHIP_07.d.ts +0 -101
- package/dist/types/ship/interfaces/ISHIP_07.d.ts +0 -522
|
@@ -1,350 +1,635 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* SHIP-06:
|
|
3
|
+
* SHIP-06: Secure Vault Implementation
|
|
4
4
|
*
|
|
5
|
-
*
|
|
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
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
this.
|
|
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.
|
|
37
|
-
this.
|
|
38
|
-
this.
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
console.
|
|
127
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return this.
|
|
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
|
-
//
|
|
108
|
+
// CRUD OPERATIONS
|
|
162
109
|
// ========================================================================
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
198
|
-
|
|
177
|
+
// Check if deleted (unless includeDeleted)
|
|
178
|
+
if (encryptedRecord.deleted && !options?.includeDeleted) {
|
|
179
|
+
return null;
|
|
199
180
|
}
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
//
|
|
377
|
+
// BACKUP & RESTORE
|
|
227
378
|
// ========================================================================
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (this.
|
|
233
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
252
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if (!this.
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
//
|
|
509
|
+
// UTILITIES
|
|
322
510
|
// ========================================================================
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
this.
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
336
|
-
|
|
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
|
-
|
|
339
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
347
|
-
|
|
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";
|