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.
- package/dist/ship/examples/ephemeral-cli.js +234 -0
- package/dist/ship/examples/identity-cli.js +503 -0
- package/dist/ship/examples/messenger-cli.js +745 -0
- package/dist/ship/examples/stealth-cli.js +433 -0
- package/dist/ship/examples/storage-cli.js +615 -0
- package/dist/ship/examples/vault-cli.js +444 -0
- package/dist/ship/examples/wallet-cli.js +767 -0
- package/dist/ship/implementation/SHIP_00.js +478 -0
- package/dist/ship/implementation/SHIP_01.js +433 -0
- package/dist/ship/implementation/SHIP_02.js +1366 -0
- package/dist/ship/implementation/SHIP_03.js +855 -0
- package/dist/ship/implementation/SHIP_04.js +589 -0
- package/dist/ship/implementation/SHIP_05.js +1064 -0
- package/dist/ship/implementation/SHIP_06.js +350 -0
- package/dist/ship/implementation/SHIP_07.js +635 -0
- package/dist/ship/index.js +17 -0
- package/dist/ship/interfaces/ISHIP_00.js +135 -0
- package/dist/ship/interfaces/ISHIP_01.js +128 -0
- package/dist/ship/interfaces/ISHIP_02.js +57 -0
- package/dist/ship/interfaces/ISHIP_03.js +61 -0
- package/dist/ship/interfaces/ISHIP_04.js +62 -0
- package/dist/ship/interfaces/ISHIP_05.js +59 -0
- package/dist/ship/interfaces/ISHIP_06.js +144 -0
- package/dist/ship/interfaces/ISHIP_07.js +194 -0
- package/dist/types/ship/examples/ephemeral-cli.d.ts +13 -0
- package/dist/types/ship/examples/identity-cli.d.ts +40 -0
- package/dist/types/ship/examples/messenger-cli.d.ts +37 -0
- package/dist/types/ship/examples/stealth-cli.d.ts +31 -0
- package/dist/types/ship/examples/storage-cli.d.ts +48 -0
- package/dist/types/ship/examples/vault-cli.d.ts +13 -0
- package/dist/types/ship/examples/wallet-cli.d.ts +131 -0
- package/dist/types/ship/implementation/SHIP_00.d.ts +113 -0
- package/dist/types/ship/implementation/SHIP_01.d.ts +80 -0
- package/dist/types/ship/implementation/SHIP_02.d.ts +297 -0
- package/dist/types/ship/implementation/SHIP_03.d.ts +127 -0
- package/dist/types/ship/implementation/SHIP_04.d.ts +76 -0
- package/dist/types/ship/implementation/SHIP_05.d.ts +70 -0
- package/dist/types/ship/implementation/SHIP_06.d.ts +66 -0
- package/dist/types/ship/implementation/SHIP_07.d.ts +101 -0
- package/dist/types/ship/index.d.ts +14 -0
- package/dist/types/ship/interfaces/ISHIP_00.d.ts +410 -0
- package/dist/types/ship/interfaces/ISHIP_01.d.ts +343 -0
- package/dist/types/ship/interfaces/ISHIP_02.d.ts +470 -0
- package/dist/types/ship/interfaces/ISHIP_03.d.ts +295 -0
- package/dist/types/ship/interfaces/ISHIP_04.d.ts +245 -0
- package/dist/types/ship/interfaces/ISHIP_05.d.ts +234 -0
- package/dist/types/ship/interfaces/ISHIP_06.d.ts +370 -0
- package/dist/types/ship/interfaces/ISHIP_07.d.ts +522 -0
- package/package.json +1 -1
- /package/dist/{config → src/config}/simplified-config.js +0 -0
- /package/dist/{core.js → src/core.js} +0 -0
- /package/dist/{examples → src/examples}/api-test.js +0 -0
- /package/dist/{examples → src/examples}/simple-api-test.js +0 -0
- /package/dist/{gundb → src/gundb}/api.js +0 -0
- /package/dist/{gundb → src/gundb}/crypto.js +0 -0
- /package/dist/{gundb → src/gundb}/db.js +0 -0
- /package/dist/{gundb → src/gundb}/derive.js +0 -0
- /package/dist/{gundb → src/gundb}/errors.js +0 -0
- /package/dist/{gundb → src/gundb}/index.js +0 -0
- /package/dist/{gundb → src/gundb}/rxjs.js +0 -0
- /package/dist/{gundb → src/gundb}/types.js +0 -0
- /package/dist/{index.js → src/index.js} +0 -0
- /package/dist/{interfaces → src/interfaces}/common.js +0 -0
- /package/dist/{interfaces → src/interfaces}/events.js +0 -0
- /package/dist/{interfaces → src/interfaces}/plugin.js +0 -0
- /package/dist/{interfaces → src/interfaces}/shogun.js +0 -0
- /package/dist/{managers → src/managers}/AuthManager.js +0 -0
- /package/dist/{managers → src/managers}/CoreInitializer.js +0 -0
- /package/dist/{managers → src/managers}/EventManager.js +0 -0
- /package/dist/{managers → src/managers}/PluginManager.js +0 -0
- /package/dist/{migration-test.js → src/migration-test.js} +0 -0
- /package/dist/{plugins → src/plugins}/base.js +0 -0
- /package/dist/{plugins → src/plugins}/index.js +0 -0
- /package/dist/{plugins → src/plugins}/nostr/index.js +0 -0
- /package/dist/{plugins → src/plugins}/nostr/nostrConnector.js +0 -0
- /package/dist/{plugins → src/plugins}/nostr/nostrConnectorPlugin.js +0 -0
- /package/dist/{plugins → src/plugins}/nostr/nostrSigner.js +0 -0
- /package/dist/{plugins → src/plugins}/nostr/types.js +0 -0
- /package/dist/{plugins → src/plugins}/oauth/index.js +0 -0
- /package/dist/{plugins → src/plugins}/oauth/oauthConnector.js +0 -0
- /package/dist/{plugins → src/plugins}/oauth/oauthPlugin.js +0 -0
- /package/dist/{plugins → src/plugins}/oauth/types.js +0 -0
- /package/dist/{plugins → src/plugins}/web3/index.js +0 -0
- /package/dist/{plugins → src/plugins}/web3/types.js +0 -0
- /package/dist/{plugins → src/plugins}/web3/web3Connector.js +0 -0
- /package/dist/{plugins → src/plugins}/web3/web3ConnectorPlugin.js +0 -0
- /package/dist/{plugins → src/plugins}/web3/web3Signer.js +0 -0
- /package/dist/{plugins → src/plugins}/webauthn/index.js +0 -0
- /package/dist/{plugins → src/plugins}/webauthn/types.js +0 -0
- /package/dist/{plugins → src/plugins}/webauthn/webauthn.js +0 -0
- /package/dist/{plugins → src/plugins}/webauthn/webauthnPlugin.js +0 -0
- /package/dist/{plugins → src/plugins}/webauthn/webauthnSigner.js +0 -0
- /package/dist/{storage → src/storage}/storage.js +0 -0
- /package/dist/{types → src/types}/events.js +0 -0
- /package/dist/{types → src/types}/shogun.js +0 -0
- /package/dist/{utils → src/utils}/errorHandler.js +0 -0
- /package/dist/{utils → src/utils}/eventEmitter.js +0 -0
- /package/dist/{utils → src/utils}/validation.js +0 -0
- /package/dist/types/{config → src/config}/simplified-config.d.ts +0 -0
- /package/dist/types/{core.d.ts → src/core.d.ts} +0 -0
- /package/dist/types/{examples → src/examples}/api-test.d.ts +0 -0
- /package/dist/types/{examples → src/examples}/simple-api-test.d.ts +0 -0
- /package/dist/types/{gundb → src/gundb}/api.d.ts +0 -0
- /package/dist/types/{gundb → src/gundb}/crypto.d.ts +0 -0
- /package/dist/types/{gundb → src/gundb}/db.d.ts +0 -0
- /package/dist/types/{gundb → src/gundb}/derive.d.ts +0 -0
- /package/dist/types/{gundb → src/gundb}/errors.d.ts +0 -0
- /package/dist/types/{gundb → src/gundb}/index.d.ts +0 -0
- /package/dist/types/{gundb → src/gundb}/rxjs.d.ts +0 -0
- /package/dist/types/{gundb → src/gundb}/types.d.ts +0 -0
- /package/dist/types/{index.d.ts → src/index.d.ts} +0 -0
- /package/dist/types/{interfaces → src/interfaces}/common.d.ts +0 -0
- /package/dist/types/{interfaces → src/interfaces}/events.d.ts +0 -0
- /package/dist/types/{interfaces → src/interfaces}/plugin.d.ts +0 -0
- /package/dist/types/{interfaces → src/interfaces}/shogun.d.ts +0 -0
- /package/dist/types/{managers → src/managers}/AuthManager.d.ts +0 -0
- /package/dist/types/{managers → src/managers}/CoreInitializer.d.ts +0 -0
- /package/dist/types/{managers → src/managers}/EventManager.d.ts +0 -0
- /package/dist/types/{managers → src/managers}/PluginManager.d.ts +0 -0
- /package/dist/types/{migration-test.d.ts → src/migration-test.d.ts} +0 -0
- /package/dist/types/{plugins → src/plugins}/base.d.ts +0 -0
- /package/dist/types/{plugins → src/plugins}/index.d.ts +0 -0
- /package/dist/types/{plugins → src/plugins}/nostr/index.d.ts +0 -0
- /package/dist/types/{plugins → src/plugins}/nostr/nostrConnector.d.ts +0 -0
- /package/dist/types/{plugins → src/plugins}/nostr/nostrConnectorPlugin.d.ts +0 -0
- /package/dist/types/{plugins → src/plugins}/nostr/nostrSigner.d.ts +0 -0
- /package/dist/types/{plugins → src/plugins}/nostr/types.d.ts +0 -0
- /package/dist/types/{plugins → src/plugins}/oauth/index.d.ts +0 -0
- /package/dist/types/{plugins → src/plugins}/oauth/oauthConnector.d.ts +0 -0
- /package/dist/types/{plugins → src/plugins}/oauth/oauthPlugin.d.ts +0 -0
- /package/dist/types/{plugins → src/plugins}/oauth/types.d.ts +0 -0
- /package/dist/types/{plugins → src/plugins}/web3/index.d.ts +0 -0
- /package/dist/types/{plugins → src/plugins}/web3/types.d.ts +0 -0
- /package/dist/types/{plugins → src/plugins}/web3/web3Connector.d.ts +0 -0
- /package/dist/types/{plugins → src/plugins}/web3/web3ConnectorPlugin.d.ts +0 -0
- /package/dist/types/{plugins → src/plugins}/web3/web3Signer.d.ts +0 -0
- /package/dist/types/{plugins → src/plugins}/webauthn/index.d.ts +0 -0
- /package/dist/types/{plugins → src/plugins}/webauthn/types.d.ts +0 -0
- /package/dist/types/{plugins → src/plugins}/webauthn/webauthn.d.ts +0 -0
- /package/dist/types/{plugins → src/plugins}/webauthn/webauthnPlugin.d.ts +0 -0
- /package/dist/types/{plugins → src/plugins}/webauthn/webauthnSigner.d.ts +0 -0
- /package/dist/types/{storage → src/storage}/storage.d.ts +0 -0
- /package/dist/types/{types → src/types}/events.d.ts +0 -0
- /package/dist/types/{types → src/types}/shogun.d.ts +0 -0
- /package/dist/types/{utils → src/utils}/errorHandler.d.ts +0 -0
- /package/dist/types/{utils → src/utils}/eventEmitter.d.ts +0 -0
- /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
|
+
};
|