shogun-core 3.3.1 → 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.
Files changed (147) hide show
  1. package/dist/ship/examples/ephemeral-cli.js +234 -0
  2. package/dist/ship/examples/identity-cli.js +503 -0
  3. package/dist/ship/examples/messenger-cli.js +745 -0
  4. package/dist/ship/examples/stealth-cli.js +433 -0
  5. package/dist/ship/examples/storage-cli.js +615 -0
  6. package/dist/ship/examples/vault-cli.js +444 -0
  7. package/dist/ship/examples/wallet-cli.js +767 -0
  8. package/dist/ship/implementation/SHIP_00.js +478 -0
  9. package/dist/ship/implementation/SHIP_01.js +433 -0
  10. package/dist/ship/implementation/SHIP_02.js +1366 -0
  11. package/dist/ship/implementation/SHIP_03.js +855 -0
  12. package/dist/ship/implementation/SHIP_04.js +589 -0
  13. package/dist/ship/implementation/SHIP_05.js +1064 -0
  14. package/dist/ship/implementation/SHIP_06.js +350 -0
  15. package/dist/ship/implementation/SHIP_07.js +635 -0
  16. package/dist/ship/index.js +17 -0
  17. package/dist/ship/interfaces/ISHIP_00.js +135 -0
  18. package/dist/ship/interfaces/ISHIP_01.js +128 -0
  19. package/dist/ship/interfaces/ISHIP_02.js +57 -0
  20. package/dist/ship/interfaces/ISHIP_03.js +61 -0
  21. package/dist/ship/interfaces/ISHIP_04.js +62 -0
  22. package/dist/ship/interfaces/ISHIP_05.js +59 -0
  23. package/dist/ship/interfaces/ISHIP_06.js +144 -0
  24. package/dist/ship/interfaces/ISHIP_07.js +194 -0
  25. package/dist/types/ship/examples/ephemeral-cli.d.ts +13 -0
  26. package/dist/types/ship/examples/identity-cli.d.ts +40 -0
  27. package/dist/types/ship/examples/messenger-cli.d.ts +37 -0
  28. package/dist/types/ship/examples/stealth-cli.d.ts +31 -0
  29. package/dist/types/ship/examples/storage-cli.d.ts +48 -0
  30. package/dist/types/ship/examples/vault-cli.d.ts +13 -0
  31. package/dist/types/ship/examples/wallet-cli.d.ts +131 -0
  32. package/dist/types/ship/implementation/SHIP_00.d.ts +113 -0
  33. package/dist/types/ship/implementation/SHIP_01.d.ts +80 -0
  34. package/dist/types/ship/implementation/SHIP_02.d.ts +297 -0
  35. package/dist/types/ship/implementation/SHIP_03.d.ts +127 -0
  36. package/dist/types/ship/implementation/SHIP_04.d.ts +76 -0
  37. package/dist/types/ship/implementation/SHIP_05.d.ts +70 -0
  38. package/dist/types/ship/implementation/SHIP_06.d.ts +66 -0
  39. package/dist/types/ship/implementation/SHIP_07.d.ts +101 -0
  40. package/dist/types/ship/index.d.ts +14 -0
  41. package/dist/types/ship/interfaces/ISHIP_00.d.ts +410 -0
  42. package/dist/types/ship/interfaces/ISHIP_01.d.ts +343 -0
  43. package/dist/types/ship/interfaces/ISHIP_02.d.ts +470 -0
  44. package/dist/types/ship/interfaces/ISHIP_03.d.ts +295 -0
  45. package/dist/types/ship/interfaces/ISHIP_04.d.ts +245 -0
  46. package/dist/types/ship/interfaces/ISHIP_05.d.ts +234 -0
  47. package/dist/types/ship/interfaces/ISHIP_06.d.ts +370 -0
  48. package/dist/types/ship/interfaces/ISHIP_07.d.ts +522 -0
  49. package/package.json +1 -1
  50. /package/dist/{config → src/config}/simplified-config.js +0 -0
  51. /package/dist/{core.js → src/core.js} +0 -0
  52. /package/dist/{examples → src/examples}/api-test.js +0 -0
  53. /package/dist/{examples → src/examples}/simple-api-test.js +0 -0
  54. /package/dist/{gundb → src/gundb}/api.js +0 -0
  55. /package/dist/{gundb → src/gundb}/crypto.js +0 -0
  56. /package/dist/{gundb → src/gundb}/db.js +0 -0
  57. /package/dist/{gundb → src/gundb}/derive.js +0 -0
  58. /package/dist/{gundb → src/gundb}/errors.js +0 -0
  59. /package/dist/{gundb → src/gundb}/index.js +0 -0
  60. /package/dist/{gundb → src/gundb}/rxjs.js +0 -0
  61. /package/dist/{gundb → src/gundb}/types.js +0 -0
  62. /package/dist/{index.js → src/index.js} +0 -0
  63. /package/dist/{interfaces → src/interfaces}/common.js +0 -0
  64. /package/dist/{interfaces → src/interfaces}/events.js +0 -0
  65. /package/dist/{interfaces → src/interfaces}/plugin.js +0 -0
  66. /package/dist/{interfaces → src/interfaces}/shogun.js +0 -0
  67. /package/dist/{managers → src/managers}/AuthManager.js +0 -0
  68. /package/dist/{managers → src/managers}/CoreInitializer.js +0 -0
  69. /package/dist/{managers → src/managers}/EventManager.js +0 -0
  70. /package/dist/{managers → src/managers}/PluginManager.js +0 -0
  71. /package/dist/{migration-test.js → src/migration-test.js} +0 -0
  72. /package/dist/{plugins → src/plugins}/base.js +0 -0
  73. /package/dist/{plugins → src/plugins}/index.js +0 -0
  74. /package/dist/{plugins → src/plugins}/nostr/index.js +0 -0
  75. /package/dist/{plugins → src/plugins}/nostr/nostrConnector.js +0 -0
  76. /package/dist/{plugins → src/plugins}/nostr/nostrConnectorPlugin.js +0 -0
  77. /package/dist/{plugins → src/plugins}/nostr/nostrSigner.js +0 -0
  78. /package/dist/{plugins → src/plugins}/nostr/types.js +0 -0
  79. /package/dist/{plugins → src/plugins}/oauth/index.js +0 -0
  80. /package/dist/{plugins → src/plugins}/oauth/oauthConnector.js +0 -0
  81. /package/dist/{plugins → src/plugins}/oauth/oauthPlugin.js +0 -0
  82. /package/dist/{plugins → src/plugins}/oauth/types.js +0 -0
  83. /package/dist/{plugins → src/plugins}/web3/index.js +0 -0
  84. /package/dist/{plugins → src/plugins}/web3/types.js +0 -0
  85. /package/dist/{plugins → src/plugins}/web3/web3Connector.js +0 -0
  86. /package/dist/{plugins → src/plugins}/web3/web3ConnectorPlugin.js +0 -0
  87. /package/dist/{plugins → src/plugins}/web3/web3Signer.js +0 -0
  88. /package/dist/{plugins → src/plugins}/webauthn/index.js +0 -0
  89. /package/dist/{plugins → src/plugins}/webauthn/types.js +0 -0
  90. /package/dist/{plugins → src/plugins}/webauthn/webauthn.js +0 -0
  91. /package/dist/{plugins → src/plugins}/webauthn/webauthnPlugin.js +0 -0
  92. /package/dist/{plugins → src/plugins}/webauthn/webauthnSigner.js +0 -0
  93. /package/dist/{storage → src/storage}/storage.js +0 -0
  94. /package/dist/{types → src/types}/events.js +0 -0
  95. /package/dist/{types → src/types}/shogun.js +0 -0
  96. /package/dist/{utils → src/utils}/errorHandler.js +0 -0
  97. /package/dist/{utils → src/utils}/eventEmitter.js +0 -0
  98. /package/dist/{utils → src/utils}/validation.js +0 -0
  99. /package/dist/types/{config → src/config}/simplified-config.d.ts +0 -0
  100. /package/dist/types/{core.d.ts → src/core.d.ts} +0 -0
  101. /package/dist/types/{examples → src/examples}/api-test.d.ts +0 -0
  102. /package/dist/types/{examples → src/examples}/simple-api-test.d.ts +0 -0
  103. /package/dist/types/{gundb → src/gundb}/api.d.ts +0 -0
  104. /package/dist/types/{gundb → src/gundb}/crypto.d.ts +0 -0
  105. /package/dist/types/{gundb → src/gundb}/db.d.ts +0 -0
  106. /package/dist/types/{gundb → src/gundb}/derive.d.ts +0 -0
  107. /package/dist/types/{gundb → src/gundb}/errors.d.ts +0 -0
  108. /package/dist/types/{gundb → src/gundb}/index.d.ts +0 -0
  109. /package/dist/types/{gundb → src/gundb}/rxjs.d.ts +0 -0
  110. /package/dist/types/{gundb → src/gundb}/types.d.ts +0 -0
  111. /package/dist/types/{index.d.ts → src/index.d.ts} +0 -0
  112. /package/dist/types/{interfaces → src/interfaces}/common.d.ts +0 -0
  113. /package/dist/types/{interfaces → src/interfaces}/events.d.ts +0 -0
  114. /package/dist/types/{interfaces → src/interfaces}/plugin.d.ts +0 -0
  115. /package/dist/types/{interfaces → src/interfaces}/shogun.d.ts +0 -0
  116. /package/dist/types/{managers → src/managers}/AuthManager.d.ts +0 -0
  117. /package/dist/types/{managers → src/managers}/CoreInitializer.d.ts +0 -0
  118. /package/dist/types/{managers → src/managers}/EventManager.d.ts +0 -0
  119. /package/dist/types/{managers → src/managers}/PluginManager.d.ts +0 -0
  120. /package/dist/types/{migration-test.d.ts → src/migration-test.d.ts} +0 -0
  121. /package/dist/types/{plugins → src/plugins}/base.d.ts +0 -0
  122. /package/dist/types/{plugins → src/plugins}/index.d.ts +0 -0
  123. /package/dist/types/{plugins → src/plugins}/nostr/index.d.ts +0 -0
  124. /package/dist/types/{plugins → src/plugins}/nostr/nostrConnector.d.ts +0 -0
  125. /package/dist/types/{plugins → src/plugins}/nostr/nostrConnectorPlugin.d.ts +0 -0
  126. /package/dist/types/{plugins → src/plugins}/nostr/nostrSigner.d.ts +0 -0
  127. /package/dist/types/{plugins → src/plugins}/nostr/types.d.ts +0 -0
  128. /package/dist/types/{plugins → src/plugins}/oauth/index.d.ts +0 -0
  129. /package/dist/types/{plugins → src/plugins}/oauth/oauthConnector.d.ts +0 -0
  130. /package/dist/types/{plugins → src/plugins}/oauth/oauthPlugin.d.ts +0 -0
  131. /package/dist/types/{plugins → src/plugins}/oauth/types.d.ts +0 -0
  132. /package/dist/types/{plugins → src/plugins}/web3/index.d.ts +0 -0
  133. /package/dist/types/{plugins → src/plugins}/web3/types.d.ts +0 -0
  134. /package/dist/types/{plugins → src/plugins}/web3/web3Connector.d.ts +0 -0
  135. /package/dist/types/{plugins → src/plugins}/web3/web3ConnectorPlugin.d.ts +0 -0
  136. /package/dist/types/{plugins → src/plugins}/web3/web3Signer.d.ts +0 -0
  137. /package/dist/types/{plugins → src/plugins}/webauthn/index.d.ts +0 -0
  138. /package/dist/types/{plugins → src/plugins}/webauthn/types.d.ts +0 -0
  139. /package/dist/types/{plugins → src/plugins}/webauthn/webauthn.d.ts +0 -0
  140. /package/dist/types/{plugins → src/plugins}/webauthn/webauthnPlugin.d.ts +0 -0
  141. /package/dist/types/{plugins → src/plugins}/webauthn/webauthnSigner.d.ts +0 -0
  142. /package/dist/types/{storage → src/storage}/storage.d.ts +0 -0
  143. /package/dist/types/{types → src/types}/events.d.ts +0 -0
  144. /package/dist/types/{types → src/types}/shogun.d.ts +0 -0
  145. /package/dist/types/{utils → src/utils}/errorHandler.d.ts +0 -0
  146. /package/dist/types/{utils → src/utils}/eventEmitter.d.ts +0 -0
  147. /package/dist/types/{utils → src/utils}/validation.d.ts +0 -0
@@ -0,0 +1,1064 @@
1
+ "use strict";
2
+ /**
3
+ * SHIP-05: Decentralized File Storage Implementation
4
+ *
5
+ * Simple encrypted file storage on IPFS.
6
+ * Extends SHIP-00 to provide encrypted file storage capabilities.
7
+ *
8
+ * Based on:
9
+ * - SHIP-00 for identity foundation
10
+ * - IPFS for decentralized file storage
11
+ * - shogun-ipfs for IPFS operations
12
+ * - Deterministic encryption from wallet signatures
13
+ *
14
+ * Features:
15
+ * ✅ Encrypted file upload with wallet signature
16
+ * ✅ Deterministic encryption keys from wallet
17
+ * ✅ IPFS storage (Pinata, IPFS node, or custom)
18
+ * ✅ File metadata on GunDB
19
+ * ✅ File download and decryption
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.SHIP_05 = void 0;
23
+ const shogun_ipfs_1 = require("shogun-ipfs");
24
+ // ============================================================================
25
+ // IMPLEMENTATION
26
+ // ============================================================================
27
+ /**
28
+ * SHIP-05 Reference Implementation
29
+ *
30
+ * Provides encrypted file storage on IPFS.
31
+ * All encryption is deterministic based on wallet signatures.
32
+ */
33
+ class SHIP_05 {
34
+ constructor(identity, config = {}) {
35
+ this.initialized = false;
36
+ // IPFS storage instance (shogun-ipfs)
37
+ // StorageService is the return type of ShogunIpfs() factory function
38
+ this.ipfsStorage = null;
39
+ // File cache (metadata)
40
+ this.fileCache = new Map();
41
+ this.identity = identity;
42
+ this.config = {
43
+ ipfsService: config.ipfsService ?? "CUSTOM",
44
+ ipfsConfig: config.ipfsConfig ?? {},
45
+ maxFileSizeMB: config.maxFileSizeMB ?? 100,
46
+ };
47
+ }
48
+ // ========================================================================
49
+ // INITIALIZATION
50
+ // ========================================================================
51
+ async initialize(options) {
52
+ if (this.initialized) {
53
+ return;
54
+ }
55
+ try {
56
+ // Ensure SHIP-00 is authenticated
57
+ if (!this.identity.isLoggedIn()) {
58
+ throw new Error("SHIP-00 identity not authenticated");
59
+ }
60
+ // Merge options with config
61
+ if (options) {
62
+ this.config = { ...this.config, ...options };
63
+ }
64
+ console.log("🔐 Initializing SHIP-05 (Decentralized Storage)...");
65
+ // Initialize IPFS storage if configured
66
+ if (this.config.ipfsService && this.config.ipfsConfig) {
67
+ await this.initializeIPFS();
68
+ }
69
+ this.initialized = true;
70
+ console.log("✅ SHIP-05 initialized successfully");
71
+ }
72
+ catch (error) {
73
+ throw new Error(`SHIP-05 initialization failed: ${error.message}`);
74
+ }
75
+ }
76
+ isInitialized() {
77
+ return this.initialized;
78
+ }
79
+ getIdentity() {
80
+ return this.identity;
81
+ }
82
+ // ========================================================================
83
+ // FILE OPERATIONS
84
+ // ========================================================================
85
+ async uploadFile(file, options = {}) {
86
+ try {
87
+ this.ensureInitialized();
88
+ const { encrypt = false, pin = true } = options;
89
+ console.log("📤 Uploading file to IPFS...");
90
+ console.log(` Encryption: ${encrypt ? "YES" : "NO"}`);
91
+ // Check file size
92
+ const fileSize = file instanceof File ? file.size : file.length;
93
+ const fileSizeMB = fileSize / (1024 * 1024);
94
+ if (fileSizeMB > this.config.maxFileSizeMB) {
95
+ return {
96
+ success: false,
97
+ error: `File too large (${fileSizeMB.toFixed(2)} MB > ${this.config.maxFileSizeMB} MB)`,
98
+ };
99
+ }
100
+ let dataToUpload = file;
101
+ let isEncrypted = false;
102
+ // Encrypt if requested
103
+ if (encrypt) {
104
+ console.log("🔐 Encrypting file with SEA...");
105
+ // Convert file to base64
106
+ let base64Data;
107
+ if (file instanceof File) {
108
+ base64Data = await this.fileToBase64(file);
109
+ }
110
+ else {
111
+ base64Data = file.toString("base64");
112
+ }
113
+ // Encrypt using SEA
114
+ const encrypted = await this.encryptData(base64Data);
115
+ dataToUpload = Buffer.from(encrypted);
116
+ isEncrypted = true;
117
+ }
118
+ // Upload to IPFS
119
+ const hash = await this.uploadToIPFS(dataToUpload);
120
+ if (!hash) {
121
+ return {
122
+ success: false,
123
+ error: "IPFS upload failed",
124
+ };
125
+ }
126
+ // Save metadata
127
+ const metadata = {
128
+ id: `${hash}-${Date.now()}`,
129
+ name: file instanceof File ? file.name : "buffer",
130
+ hash,
131
+ sizeMB: Math.ceil(fileSizeMB * 100) / 100,
132
+ uploadedAt: Date.now(),
133
+ encrypted: isEncrypted,
134
+ type: file instanceof File ? file.type : undefined,
135
+ };
136
+ await this.saveFileMetadata(metadata);
137
+ console.log(`✅ File uploaded successfully: ${hash}`);
138
+ return {
139
+ success: true,
140
+ hash,
141
+ size: fileSize,
142
+ encrypted: isEncrypted,
143
+ };
144
+ }
145
+ catch (error) {
146
+ console.error("❌ Error uploading file:", error);
147
+ return {
148
+ success: false,
149
+ error: error.message,
150
+ };
151
+ }
152
+ }
153
+ async uploadJson(data, options = {}) {
154
+ try {
155
+ const jsonString = JSON.stringify(data);
156
+ const buffer = Buffer.from(jsonString, "utf-8");
157
+ return await this.uploadFile(buffer, options);
158
+ }
159
+ catch (error) {
160
+ return {
161
+ success: false,
162
+ error: error.message,
163
+ };
164
+ }
165
+ }
166
+ async downloadFile(hash, options = {}) {
167
+ try {
168
+ this.ensureInitialized();
169
+ const { decrypt = false, returnBlob = false } = options;
170
+ console.log(`📥 Downloading file: ${hash}`);
171
+ // Get file metadata to check if encrypted
172
+ const metadata = await this.getFileMetadata(hash);
173
+ const shouldDecrypt = decrypt && metadata?.encrypted;
174
+ // Download from IPFS
175
+ const data = await this.downloadFromIPFS(hash);
176
+ if (!data) {
177
+ throw new Error("File not found on IPFS");
178
+ }
179
+ // Decrypt if needed
180
+ if (shouldDecrypt) {
181
+ console.log("🔐 Decrypting file with SEA...");
182
+ const decrypted = await this.decryptData(data);
183
+ if (returnBlob) {
184
+ // Convert base64 to blob
185
+ const base64 = decrypted.includes(",")
186
+ ? decrypted.split(",")[1]
187
+ : decrypted;
188
+ const binary = atob(base64);
189
+ const bytes = new Uint8Array(binary.length);
190
+ for (let i = 0; i < binary.length; i++) {
191
+ bytes[i] = binary.charCodeAt(i);
192
+ }
193
+ return new Blob([bytes]);
194
+ }
195
+ return decrypted;
196
+ }
197
+ if (returnBlob && typeof data === "string") {
198
+ // Convert to blob
199
+ return new Blob([data]);
200
+ }
201
+ return data;
202
+ }
203
+ catch (error) {
204
+ console.error("❌ Error downloading file:", error);
205
+ throw error;
206
+ }
207
+ }
208
+ async getFileMetadata(hash) {
209
+ // Check cache first
210
+ if (this.fileCache.has(hash)) {
211
+ return this.fileCache.get(hash);
212
+ }
213
+ // Try to load from Gun
214
+ try {
215
+ const shogun = this.identity.getShogun();
216
+ const gun = shogun?.db?.gun;
217
+ if (!gun)
218
+ return null;
219
+ const user = gun.user();
220
+ if (!user || !user.is)
221
+ return null;
222
+ const files = await this.getUserFilesFromGun();
223
+ const metadata = files.find((f) => f.hash === hash);
224
+ if (metadata) {
225
+ this.fileCache.set(hash, metadata);
226
+ }
227
+ return metadata || null;
228
+ }
229
+ catch (error) {
230
+ console.error("Error loading file metadata:", error);
231
+ return null;
232
+ }
233
+ }
234
+ async deleteFile(hash) {
235
+ try {
236
+ this.ensureInitialized();
237
+ console.log(`🗑️ Deleting file: ${hash}`);
238
+ // Try to unpin from IPFS
239
+ const unpinned = await this.unpinFromIPFS(hash);
240
+ if (unpinned) {
241
+ console.log("✅ File unpinned from IPFS");
242
+ }
243
+ else {
244
+ console.warn("⚠️ Could not unpin from IPFS (file may not be pinned)");
245
+ }
246
+ // Remove from Gun
247
+ await this.removeFileMetadata(hash);
248
+ // Update cache
249
+ this.fileCache.delete(hash);
250
+ console.log("✅ File deleted successfully");
251
+ return { success: true };
252
+ }
253
+ catch (error) {
254
+ console.error("❌ Error deleting file:", error);
255
+ return {
256
+ success: false,
257
+ error: error.message,
258
+ };
259
+ }
260
+ }
261
+ async getUserFiles() {
262
+ try {
263
+ return await this.getUserFilesFromGun();
264
+ }
265
+ catch (error) {
266
+ console.error("Error getting user files:", error);
267
+ return [];
268
+ }
269
+ }
270
+ // ========================================================================
271
+ // ENCRYPTION (uses SEA from SHIP-00)
272
+ // ========================================================================
273
+ async encryptData(data, options = {}) {
274
+ try {
275
+ this.ensureInitialized();
276
+ // Get SEA pair from SHIP-00
277
+ const seaPair = this.identity.getKeyPair();
278
+ if (!seaPair) {
279
+ throw new Error("SEA keypair not available. User not authenticated.");
280
+ }
281
+ // Access crypto from Shogun Core
282
+ const shogun = this.identity.getShogun();
283
+ const crypto = shogun?.db?.crypto;
284
+ if (!crypto) {
285
+ throw new Error("Crypto not available from Shogun Core");
286
+ }
287
+ // Convert Buffer to string if needed
288
+ const dataString = typeof data === "string" ? data : data.toString("base64");
289
+ console.log(`🔐 Encrypting data with SEA (length: ${dataString.length})...`);
290
+ // Use SEA encryption with user's keypair
291
+ const encrypted = await crypto.encrypt(dataString, seaPair);
292
+ console.log(`✅ Data encrypted with SEA`);
293
+ return JSON.stringify(encrypted);
294
+ }
295
+ catch (error) {
296
+ console.error("❌ Error encrypting data:", error);
297
+ throw error;
298
+ }
299
+ }
300
+ async decryptData(encryptedData, options = {}) {
301
+ try {
302
+ this.ensureInitialized();
303
+ // Get SEA pair from SHIP-00
304
+ const seaPair = this.identity.getKeyPair();
305
+ if (!seaPair) {
306
+ throw new Error("SEA keypair not available. User not authenticated.");
307
+ }
308
+ // Access crypto from Shogun Core
309
+ const shogun = this.identity.getShogun();
310
+ const crypto = shogun?.db?.crypto;
311
+ if (!crypto) {
312
+ throw new Error("Crypto not available from Shogun Core");
313
+ }
314
+ console.log(`🔓 Decrypting data with SEA...`);
315
+ // Parse encrypted data
316
+ const encrypted = JSON.parse(encryptedData);
317
+ // Use SEA decryption with user's keypair
318
+ const decrypted = await crypto.decrypt(encrypted, seaPair);
319
+ console.log(`✅ Data decrypted with SEA (length: ${decrypted.length})`);
320
+ return decrypted;
321
+ }
322
+ catch (error) {
323
+ console.error("❌ Error decrypting data:", error);
324
+ throw error;
325
+ }
326
+ }
327
+ // ========================================================================
328
+ // UTILITIES
329
+ // ========================================================================
330
+ async isFileAccessible(hash) {
331
+ try {
332
+ if (!this.ipfsStorage ||
333
+ typeof this.ipfsStorage.getMetadata !== "function") {
334
+ return false;
335
+ }
336
+ // Try to get metadata
337
+ const metadata = await this.ipfsStorage.getMetadata(hash);
338
+ return !!metadata;
339
+ }
340
+ catch {
341
+ return false;
342
+ }
343
+ }
344
+ async getStorageStats() {
345
+ try {
346
+ const files = await this.getUserFiles();
347
+ const encrypted = files.filter((f) => f.encrypted);
348
+ const plain = files.filter((f) => !f.encrypted);
349
+ const totalMB = files.reduce((sum, f) => sum + f.sizeMB, 0);
350
+ return {
351
+ totalFiles: files.length,
352
+ totalMB,
353
+ encryptedFiles: encrypted.length,
354
+ plainFiles: plain.length,
355
+ };
356
+ }
357
+ catch (error) {
358
+ console.error("Error getting storage stats:", error);
359
+ return {
360
+ totalFiles: 0,
361
+ totalMB: 0,
362
+ encryptedFiles: 0,
363
+ plainFiles: 0,
364
+ };
365
+ }
366
+ }
367
+ // ========================================================================
368
+ // PRIVATE HELPERS
369
+ // ========================================================================
370
+ ensureInitialized() {
371
+ if (!this.initialized) {
372
+ throw new Error("SHIP-05 not initialized. Call initialize() first.");
373
+ }
374
+ }
375
+ async initializeIPFS() {
376
+ try {
377
+ const service = this.config.ipfsService;
378
+ // Build config for shogun-ipfs (now supports all 3 services!)
379
+ let serviceConfig = {};
380
+ if (service === "PINATA") {
381
+ serviceConfig = {
382
+ pinataJwt: this.config.ipfsConfig?.pinataJwt,
383
+ pinataGateway: this.config.ipfsConfig?.pinataGateway,
384
+ };
385
+ }
386
+ else if (service === "IPFS-CLIENT") {
387
+ serviceConfig = {
388
+ url: this.config.ipfsConfig?.url || "http://localhost:5001",
389
+ };
390
+ }
391
+ else if (service === "CUSTOM") {
392
+ serviceConfig = {
393
+ url: this.config.ipfsConfig?.customApiUrl || "https://ipfs.io",
394
+ token: this.config.ipfsConfig?.customToken,
395
+ };
396
+ }
397
+ const config = {
398
+ service: service,
399
+ config: serviceConfig,
400
+ };
401
+ console.log("🔧 Initializing shogun-ipfs with config:", config);
402
+ // Initialize shogun-ipfs (returns StorageService)
403
+ // Supports PINATA, IPFS-CLIENT, and CUSTOM!
404
+ this.ipfsStorage = (0, shogun_ipfs_1.ShogunIpfs)(config);
405
+ console.log("✅ shogun-ipfs initialized");
406
+ }
407
+ catch (error) {
408
+ console.warn("⚠️ shogun-ipfs initialization failed, using fallback methods");
409
+ console.warn(" Error:", error);
410
+ // Set to null so fallback is used
411
+ this.ipfsStorage = null;
412
+ }
413
+ }
414
+ async uploadToIPFS(data) {
415
+ try {
416
+ // Try shogun-ipfs first (now supports uploadBuffer for all services!)
417
+ if (this.ipfsStorage && typeof this.ipfsStorage.uploadBuffer === 'function') {
418
+ try {
419
+ console.log("📤 Uploading via shogun-ipfs...");
420
+ const buffer = data instanceof File ? Buffer.from(await data.arrayBuffer()) : data;
421
+ const result = await this.ipfsStorage.uploadBuffer(buffer, {
422
+ filename: data instanceof File ? data.name : "file.bin"
423
+ });
424
+ console.log(`✅ Upload successful via shogun-ipfs: ${result.id}`);
425
+ return result.id;
426
+ }
427
+ catch (error) {
428
+ console.warn("⚠️ shogun-ipfs upload failed, using fallback:", error);
429
+ // Continue to fallback
430
+ }
431
+ }
432
+ // Fallback: direct API calls
433
+ console.log("📤 Uploading via direct API fallback...");
434
+ return await this.uploadToIPFSFallback(data);
435
+ }
436
+ catch (error) {
437
+ console.error("Error uploading to IPFS:", error);
438
+ return null;
439
+ }
440
+ }
441
+ async uploadToIPFSFallback(data) {
442
+ try {
443
+ const service = this.config.ipfsService;
444
+ // Check if running in Node.js or browser
445
+ const isNode = typeof window === "undefined";
446
+ if (service === "PINATA" && this.config.ipfsConfig?.pinataJwt) {
447
+ // Pinata upload
448
+ if (isNode) {
449
+ // Node.js environment
450
+ const FormData = require("form-data");
451
+ const formData = new FormData();
452
+ formData.append("file", data, { filename: "file" });
453
+ const response = await fetch("https://api.pinata.cloud/pinning/pinFileToIPFS", {
454
+ method: "POST",
455
+ headers: {
456
+ Authorization: `Bearer ${this.config.ipfsConfig.pinataJwt}`,
457
+ ...formData.getHeaders(),
458
+ },
459
+ body: formData,
460
+ });
461
+ const result = await response.json();
462
+ return result.IpfsHash;
463
+ }
464
+ else {
465
+ // Browser environment
466
+ const formData = new FormData();
467
+ let blob;
468
+ if (data instanceof File) {
469
+ blob = data;
470
+ }
471
+ else {
472
+ // Convert Buffer to Uint8Array for browser
473
+ blob = new Blob([new Uint8Array(data)]);
474
+ }
475
+ formData.append("file", blob);
476
+ const response = await fetch("https://api.pinata.cloud/pinning/pinFileToIPFS", {
477
+ method: "POST",
478
+ headers: {
479
+ Authorization: `Bearer ${this.config.ipfsConfig.pinataJwt}`,
480
+ },
481
+ body: formData,
482
+ });
483
+ const result = await response.json();
484
+ return result.IpfsHash;
485
+ }
486
+ }
487
+ else if (service === "IPFS-CLIENT") {
488
+ // IPFS node upload
489
+ const url = this.config.ipfsConfig?.url || "http://localhost:5001";
490
+ if (isNode) {
491
+ const FormData = require("form-data");
492
+ const formData = new FormData();
493
+ formData.append("file", data);
494
+ const response = await fetch(`${url}/api/v0/add`, {
495
+ method: "POST",
496
+ headers: formData.getHeaders(),
497
+ body: formData,
498
+ });
499
+ if (!response.ok) {
500
+ const errorText = await response.text();
501
+ throw new Error(`IPFS upload failed (${response.status}): ${errorText}`);
502
+ }
503
+ const responseText = await response.text();
504
+ // Try to parse JSON response
505
+ let result;
506
+ try {
507
+ result = JSON.parse(responseText);
508
+ }
509
+ catch (parseError) {
510
+ console.error("❌ Response is not JSON:", responseText.substring(0, 200));
511
+ throw new Error(`Invalid IPFS response: ${responseText.substring(0, 100)}`);
512
+ }
513
+ return result.Hash;
514
+ }
515
+ else {
516
+ const formData = new FormData();
517
+ let blob;
518
+ if (data instanceof File) {
519
+ blob = data;
520
+ }
521
+ else {
522
+ blob = new Blob([new Uint8Array(data)]);
523
+ }
524
+ formData.append("file", blob);
525
+ const response = await fetch(`${url}/api/v0/add`, {
526
+ method: "POST",
527
+ body: formData,
528
+ });
529
+ const result = await response.json();
530
+ return result.Hash;
531
+ }
532
+ }
533
+ else if (service === "CUSTOM") {
534
+ // Custom API - generic IPFS endpoint support
535
+ let apiUrl = this.config.ipfsConfig?.customApiUrl || "https://ipfs.io";
536
+ // Remove trailing slash to avoid double slashes
537
+ apiUrl = apiUrl.replace(/\/+$/, '');
538
+ const token = this.config.ipfsConfig?.customToken;
539
+ if (isNode) {
540
+ // Use http.request with form-data for Node.js
541
+ const https = require('https');
542
+ const http = require('http');
543
+ const FormData = require("form-data");
544
+ const { URL } = require('url');
545
+ const filename = data instanceof Buffer ? "encrypted-file.bin" : "file.bin";
546
+ console.log(`📦 Preparing upload:`);
547
+ console.log(` Data type: ${data instanceof Buffer ? 'Buffer' : typeof data}`);
548
+ console.log(` Data length: ${data instanceof Buffer ? data.length : data.size || 'unknown'}`);
549
+ console.log(` Filename: ${filename}`);
550
+ // Helper to try upload with specific endpoint
551
+ const tryUpload = (endpoint) => {
552
+ return new Promise((resolve, reject) => {
553
+ const fullUrl = `${apiUrl}${endpoint}`;
554
+ const parsedUrl = new URL(fullUrl);
555
+ const isHttps = parsedUrl.protocol === 'https:';
556
+ const httpModule = isHttps ? https : http;
557
+ const formData = new FormData();
558
+ formData.append("file", data, {
559
+ filename: filename,
560
+ contentType: "application/octet-stream"
561
+ });
562
+ const headers = formData.getHeaders();
563
+ if (token) {
564
+ headers["Authorization"] = `Bearer ${token}`;
565
+ headers["token"] = token;
566
+ }
567
+ console.log(`📡 Trying ${fullUrl}...`);
568
+ const options = {
569
+ hostname: parsedUrl.hostname,
570
+ port: parsedUrl.port || (isHttps ? 443 : 80),
571
+ path: parsedUrl.pathname + parsedUrl.search,
572
+ method: 'POST',
573
+ headers: headers
574
+ };
575
+ const request = httpModule.request(options, (response) => {
576
+ let responseData = '';
577
+ response.on('data', (chunk) => responseData += chunk);
578
+ response.on('end', () => {
579
+ console.log(`📡 ${endpoint} response: ${response.statusCode}`);
580
+ if (response.statusCode === 200) {
581
+ try {
582
+ const result = JSON.parse(responseData);
583
+ // Support multiple response formats
584
+ const hash = result.file?.hash || result.hash || result.Hash || result.cid || result.IpfsHash;
585
+ if (hash) {
586
+ resolve(hash);
587
+ }
588
+ else {
589
+ reject(new Error(`No hash in response from ${endpoint}`));
590
+ }
591
+ }
592
+ catch (parseError) {
593
+ reject(new Error(`Invalid JSON from ${endpoint}: ${responseData.substring(0, 100)}`));
594
+ }
595
+ }
596
+ else {
597
+ reject(new Error(`${endpoint} failed (${response.statusCode}): ${responseData.substring(0, 100)}`));
598
+ }
599
+ });
600
+ });
601
+ request.on('error', (error) => reject(error));
602
+ formData.pipe(request);
603
+ });
604
+ };
605
+ // Try common IPFS endpoints in order
606
+ const endpoints = [
607
+ '/upload', // Relay-style endpoint
608
+ '/api/v0/add', // Standard IPFS API
609
+ '/add', // Simplified IPFS endpoint
610
+ ];
611
+ for (const endpoint of endpoints) {
612
+ try {
613
+ const hash = await tryUpload(endpoint);
614
+ console.log(`✅ Upload successful via ${endpoint}! Hash: ${hash}`);
615
+ return hash;
616
+ }
617
+ catch (error) {
618
+ console.log(`⚠️ ${endpoint} failed: ${error.message}`);
619
+ // Try next endpoint
620
+ }
621
+ }
622
+ throw new Error('All upload endpoints failed. Check API URL and token.');
623
+ }
624
+ else {
625
+ // Browser environment
626
+ let apiUrl = this.config.ipfsConfig?.customApiUrl || "https://ipfs.io";
627
+ apiUrl = apiUrl.replace(/\/+$/, ''); // Remove trailing slash
628
+ const formData = new FormData();
629
+ let blob;
630
+ if (data instanceof File) {
631
+ blob = data;
632
+ }
633
+ else {
634
+ // Convert Buffer to Uint8Array for browser
635
+ blob = new Blob([new Uint8Array(data)]);
636
+ }
637
+ formData.append("file", blob);
638
+ const headers = {};
639
+ if (token) {
640
+ headers["Authorization"] = `Bearer ${token}`;
641
+ headers["token"] = token; // shogun-relay also checks 'token' header
642
+ }
643
+ // Try /upload first
644
+ let response = await fetch(`${apiUrl}/upload`, {
645
+ method: "POST",
646
+ headers,
647
+ body: formData,
648
+ });
649
+ // Fallback to /add (with auth headers!)
650
+ if (!response.ok) {
651
+ const headers2 = {};
652
+ if (token) {
653
+ headers2["Authorization"] = `Bearer ${token}`;
654
+ headers2["token"] = token;
655
+ }
656
+ response = await fetch(`${apiUrl}/add`, {
657
+ method: "POST",
658
+ headers: headers2,
659
+ body: formData,
660
+ });
661
+ }
662
+ if (!response.ok) {
663
+ const errorText = await response.text();
664
+ throw new Error(`Upload failed (${response.status}): ${errorText.substring(0, 200)}`);
665
+ }
666
+ const responseText = await response.text();
667
+ // Try to parse JSON
668
+ let result;
669
+ try {
670
+ result = JSON.parse(responseText);
671
+ }
672
+ catch (parseError) {
673
+ console.error("❌ Response is not JSON:", responseText.substring(0, 200));
674
+ throw new Error(`Invalid response: ${responseText.substring(0, 100)}`);
675
+ }
676
+ return (result.hash ||
677
+ result.Hash ||
678
+ result.cid ||
679
+ result.ipfsHash ||
680
+ result.IpfsHash);
681
+ }
682
+ }
683
+ throw new Error(`Unsupported IPFS service: ${service}`);
684
+ }
685
+ catch (error) {
686
+ console.error("Fallback upload failed:", error);
687
+ console.error("💡 Consider installing shogun-ipfs: yarn add shogun-ipfs");
688
+ throw error;
689
+ }
690
+ }
691
+ async unpinFromIPFS(hash) {
692
+ try {
693
+ console.log(`📍 Unpinning ${hash} from IPFS...`);
694
+ // Try shogun-ipfs first (now supports CUSTOM gateway!)
695
+ if (this.ipfsStorage && typeof this.ipfsStorage.unpin === "function") {
696
+ try {
697
+ const unpinned = await this.ipfsStorage.unpin(hash);
698
+ if (unpinned) {
699
+ console.log("✅ File unpinned via shogun-ipfs");
700
+ return true;
701
+ }
702
+ else {
703
+ console.log("ℹ️ File not found or already unpinned");
704
+ return false;
705
+ }
706
+ }
707
+ catch (error) {
708
+ console.warn("⚠️ shogun-ipfs unpin failed, using fallback");
709
+ }
710
+ }
711
+ const service = this.config.ipfsService;
712
+ const isNode = typeof window === "undefined";
713
+ // Fallback: use API directly
714
+ if (service === "CUSTOM" && this.config.ipfsConfig?.customApiUrl) {
715
+ const baseUrl = this.config.ipfsConfig.customApiUrl.replace(/\/+$/, '');
716
+ const token = this.config.ipfsConfig?.customToken;
717
+ if (isNode) {
718
+ // Node.js - use http.request
719
+ const https = require('https');
720
+ const http = require('http');
721
+ const { URL } = require('url');
722
+ const fullUrl = `${baseUrl}/pins/rm`;
723
+ const parsedUrl = new URL(fullUrl);
724
+ const isHttps = parsedUrl.protocol === 'https:';
725
+ const httpModule = isHttps ? https : http;
726
+ return new Promise((resolve) => {
727
+ console.log(`📡 Unpinning via ${fullUrl}...`);
728
+ const postData = JSON.stringify({ cid: hash });
729
+ const options = {
730
+ hostname: parsedUrl.hostname,
731
+ port: parsedUrl.port || (isHttps ? 443 : 80),
732
+ path: parsedUrl.pathname,
733
+ method: 'POST',
734
+ headers: {
735
+ 'Content-Type': 'application/json',
736
+ 'Content-Length': Buffer.byteLength(postData)
737
+ }
738
+ };
739
+ if (token) {
740
+ options.headers["Authorization"] = `Bearer ${token}`;
741
+ options.headers["token"] = token;
742
+ }
743
+ const request = httpModule.request(options, (response) => {
744
+ let data = '';
745
+ response.on('data', (chunk) => data += chunk);
746
+ response.on('end', () => {
747
+ if (response.statusCode === 200) {
748
+ console.log(`✅ Unpinned successfully`);
749
+ resolve(true);
750
+ }
751
+ else {
752
+ console.warn(`⚠️ Unpin failed (${response.statusCode}): ${data.substring(0, 100)}`);
753
+ resolve(false);
754
+ }
755
+ });
756
+ });
757
+ request.on('error', (error) => {
758
+ console.warn(`⚠️ Unpin request error:`, error.message);
759
+ resolve(false);
760
+ });
761
+ request.write(postData);
762
+ request.end();
763
+ });
764
+ }
765
+ else {
766
+ // Browser - use fetch
767
+ const fullUrl = `${baseUrl}/pins/rm`;
768
+ console.log(`📡 Unpinning via ${fullUrl}...`);
769
+ const headers = {
770
+ 'Content-Type': 'application/json'
771
+ };
772
+ if (token) {
773
+ headers["Authorization"] = `Bearer ${token}`;
774
+ headers["token"] = token;
775
+ }
776
+ try {
777
+ const response = await fetch(fullUrl, {
778
+ method: 'POST',
779
+ headers,
780
+ body: JSON.stringify({ cid: hash })
781
+ });
782
+ if (response.ok) {
783
+ console.log(`✅ Unpinned successfully`);
784
+ return true;
785
+ }
786
+ else {
787
+ console.warn(`⚠️ Unpin failed (${response.status})`);
788
+ return false;
789
+ }
790
+ }
791
+ catch (error) {
792
+ console.warn(`⚠️ Unpin error:`, error.message);
793
+ return false;
794
+ }
795
+ }
796
+ }
797
+ else if (service === "IPFS-CLIENT") {
798
+ // Standard IPFS API unpin
799
+ const url = this.config.ipfsConfig?.url || "http://localhost:5001";
800
+ try {
801
+ const response = await fetch(`${url}/api/v0/pin/rm?arg=${hash}`, {
802
+ method: 'POST'
803
+ });
804
+ return response.ok;
805
+ }
806
+ catch (error) {
807
+ console.warn("⚠️ IPFS unpin error:", error);
808
+ return false;
809
+ }
810
+ }
811
+ return false;
812
+ }
813
+ catch (error) {
814
+ console.error("Error unpinning from IPFS:", error);
815
+ return false;
816
+ }
817
+ }
818
+ async downloadFromIPFS(hash) {
819
+ try {
820
+ // Try shogun-ipfs first (now supports CUSTOM gateway!)
821
+ if (this.ipfsStorage && typeof this.ipfsStorage.get === 'function') {
822
+ try {
823
+ console.log("📥 Downloading via shogun-ipfs...");
824
+ const result = await this.ipfsStorage.get(hash);
825
+ console.log("✅ Download successful via shogun-ipfs");
826
+ return typeof result.data === "string" ? result.data : JSON.stringify(result.data);
827
+ }
828
+ catch (error) {
829
+ console.warn("⚠️ shogun-ipfs download failed, using fallback:", error);
830
+ // Continue to fallback
831
+ }
832
+ }
833
+ // Fallback: use configured gateway or public IPFS
834
+ console.log("⚠️ Using fallback IPFS download");
835
+ const service = this.config.ipfsService;
836
+ const token = this.config.ipfsConfig?.customToken;
837
+ if (service === "PINATA" && this.config.ipfsConfig?.pinataGateway) {
838
+ // Pinata gateway (public, no auth needed)
839
+ const gatewayUrl = `${this.config.ipfsConfig.pinataGateway}/ipfs/${hash}`;
840
+ console.log(`📥 Downloading from Pinata: ${gatewayUrl}`);
841
+ const response = await fetch(gatewayUrl);
842
+ if (!response.ok)
843
+ throw new Error(`HTTP ${response.status}`);
844
+ return await response.text();
845
+ }
846
+ else if (service === "IPFS-CLIENT") {
847
+ // IPFS node API
848
+ const url = this.config.ipfsConfig?.url || "http://localhost:5001";
849
+ const gatewayUrl = `${url}/api/v0/cat?arg=${hash}`;
850
+ console.log(`📥 Downloading from IPFS node: ${gatewayUrl}`);
851
+ const headers = {};
852
+ if (token)
853
+ headers["Authorization"] = `Bearer ${token}`;
854
+ const response = await fetch(gatewayUrl, {
855
+ method: 'POST',
856
+ headers
857
+ });
858
+ if (!response.ok)
859
+ throw new Error(`HTTP ${response.status}`);
860
+ return await response.text();
861
+ }
862
+ else if (service === "CUSTOM" && this.config.ipfsConfig?.customApiUrl) {
863
+ // Custom gateway/relay - try multiple endpoints
864
+ const baseUrl = this.config.ipfsConfig.customApiUrl.replace(/\/+$/, '');
865
+ const isNode = typeof window === "undefined";
866
+ const endpoints = [
867
+ { path: `/content/${hash}`, method: 'GET' }, // Relay format
868
+ { path: `/ipfs/${hash}`, method: 'GET' }, // Gateway format
869
+ { path: `/api/v0/cat?arg=${hash}`, method: 'POST' } // IPFS API format
870
+ ];
871
+ if (isNode) {
872
+ // Use http.request for Node.js (more reliable than fetch)
873
+ const https = require('https');
874
+ const http = require('http');
875
+ const { URL } = require('url');
876
+ for (const endpoint of endpoints) {
877
+ try {
878
+ const fullUrl = `${baseUrl}${endpoint.path}`;
879
+ const parsedUrl = new URL(fullUrl);
880
+ const isHttps = parsedUrl.protocol === 'https:';
881
+ const httpModule = isHttps ? https : http;
882
+ const result = await new Promise((resolve, reject) => {
883
+ console.log(`📥 Trying ${fullUrl}...`);
884
+ const options = {
885
+ hostname: parsedUrl.hostname,
886
+ port: parsedUrl.port || (isHttps ? 443 : 80),
887
+ path: parsedUrl.pathname + parsedUrl.search,
888
+ method: endpoint.method,
889
+ headers: {}
890
+ };
891
+ if (token) {
892
+ options.headers["Authorization"] = `Bearer ${token}`;
893
+ options.headers["token"] = token;
894
+ }
895
+ if (endpoint.method === 'POST') {
896
+ options.headers["Content-Length"] = "0";
897
+ }
898
+ const request = httpModule.request(options, (response) => {
899
+ let data = '';
900
+ response.on('data', (chunk) => data += chunk);
901
+ response.on('end', () => {
902
+ if (response.statusCode === 200) {
903
+ console.log(`✅ Download successful via ${endpoint.path}`);
904
+ resolve(data);
905
+ }
906
+ else {
907
+ reject(new Error(`HTTP ${response.statusCode}`));
908
+ }
909
+ });
910
+ });
911
+ request.on('error', (error) => reject(error));
912
+ request.end();
913
+ });
914
+ return result;
915
+ }
916
+ catch (error) {
917
+ console.log(`⚠️ ${endpoint.path} failed: ${error.message}`);
918
+ }
919
+ }
920
+ throw new Error('All download endpoints failed');
921
+ }
922
+ else {
923
+ // Browser - use fetch
924
+ for (const endpoint of endpoints) {
925
+ try {
926
+ const url = `${baseUrl}${endpoint.path}`;
927
+ console.log(`📥 Trying download from: ${url}`);
928
+ const headers = {};
929
+ if (token) {
930
+ headers["Authorization"] = `Bearer ${token}`;
931
+ headers["token"] = token;
932
+ }
933
+ const response = await fetch(url, {
934
+ method: endpoint.method,
935
+ headers
936
+ });
937
+ if (response.ok) {
938
+ console.log(`✅ Download successful via ${endpoint.path}`);
939
+ return await response.text();
940
+ }
941
+ else {
942
+ console.log(`⚠️ ${endpoint.path} failed (${response.status})`);
943
+ }
944
+ }
945
+ catch (error) {
946
+ console.log(`⚠️ ${endpoint.path} error: ${error.message}`);
947
+ }
948
+ }
949
+ throw new Error('All download endpoints failed');
950
+ }
951
+ }
952
+ else {
953
+ // Default public gateway
954
+ const gatewayUrl = `https://ipfs.io/ipfs/${hash}`;
955
+ console.log(`📥 Downloading from public gateway: ${gatewayUrl}`);
956
+ const response = await fetch(gatewayUrl);
957
+ if (!response.ok)
958
+ throw new Error(`HTTP ${response.status}`);
959
+ return await response.text();
960
+ }
961
+ }
962
+ catch (error) {
963
+ console.error("Error downloading from IPFS:", error);
964
+ return null;
965
+ }
966
+ }
967
+ async saveFileMetadata(metadata) {
968
+ try {
969
+ // Save to Gun
970
+ const shogun = this.identity.getShogun();
971
+ const gun = shogun?.db?.gun;
972
+ if (!gun) {
973
+ console.warn("Gun not available, metadata not persisted");
974
+ return;
975
+ }
976
+ const user = gun.user();
977
+ if (!user || !user.is) {
978
+ console.warn("User not authenticated on Gun");
979
+ return;
980
+ }
981
+ // Get current files
982
+ const files = await this.getUserFilesFromGun();
983
+ files.push(metadata);
984
+ // Save updated list
985
+ await user.get(SHIP_05.NODES.USER_FILES).put(JSON.stringify(files));
986
+ // Update cache
987
+ this.fileCache.set(metadata.hash, metadata);
988
+ console.log("✅ File metadata saved to GunDB");
989
+ }
990
+ catch (error) {
991
+ console.error("Error saving file metadata:", error);
992
+ }
993
+ }
994
+ async removeFileMetadata(hash) {
995
+ try {
996
+ const shogun = this.identity.getShogun();
997
+ const gun = shogun?.db?.gun;
998
+ if (!gun)
999
+ return;
1000
+ const user = gun.user();
1001
+ if (!user || !user.is)
1002
+ return;
1003
+ // Get current files
1004
+ const files = await this.getUserFilesFromGun();
1005
+ // Remove the file
1006
+ const updatedFiles = files.filter((f) => f.hash !== hash);
1007
+ // Save updated list
1008
+ await user
1009
+ .get(SHIP_05.NODES.USER_FILES)
1010
+ .put(JSON.stringify(updatedFiles));
1011
+ console.log("✅ File metadata removed from GunDB");
1012
+ }
1013
+ catch (error) {
1014
+ console.error("Error removing file metadata:", error);
1015
+ }
1016
+ }
1017
+ async getUserFilesFromGun() {
1018
+ try {
1019
+ const shogun = this.identity.getShogun();
1020
+ const gun = shogun?.db?.gun;
1021
+ if (!gun)
1022
+ return [];
1023
+ const user = gun.user();
1024
+ if (!user || !user.is)
1025
+ return [];
1026
+ const data = await new Promise((resolve) => {
1027
+ let resolved = false;
1028
+ const timeout = setTimeout(() => {
1029
+ if (!resolved) {
1030
+ resolved = true;
1031
+ resolve(null);
1032
+ }
1033
+ }, 5000);
1034
+ user.get(SHIP_05.NODES.USER_FILES).once((data) => {
1035
+ if (!resolved) {
1036
+ resolved = true;
1037
+ clearTimeout(timeout);
1038
+ resolve(data || null);
1039
+ }
1040
+ });
1041
+ });
1042
+ if (!data)
1043
+ return [];
1044
+ return JSON.parse(data);
1045
+ }
1046
+ catch (error) {
1047
+ console.error("Error loading files from Gun:", error);
1048
+ return [];
1049
+ }
1050
+ }
1051
+ async fileToBase64(file) {
1052
+ return new Promise((resolve, reject) => {
1053
+ const reader = new FileReader();
1054
+ reader.onload = (e) => resolve(e.target?.result);
1055
+ reader.onerror = reject;
1056
+ reader.readAsDataURL(file);
1057
+ });
1058
+ }
1059
+ }
1060
+ exports.SHIP_05 = SHIP_05;
1061
+ // GunDB Node Names for SHIP-05 storage
1062
+ SHIP_05.NODES = {
1063
+ USER_FILES: "user_files", // User's file metadata
1064
+ };