@le-space/orbitdb-storage-bridge 0.10.0
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/LICENSE +21 -0
- package/README.md +290 -0
- package/dist/components/StorachaAuth.svelte +801 -0
- package/dist/components/StorachaIntegration.svelte +965 -0
- package/dist/components/WebAuthnDIDProvider.js +562 -0
- package/dist/components/storacha-backup.js +325 -0
- package/dist/components/theme.js +79 -0
- package/lib/backends/aleph-pin.js +161 -0
- package/lib/backends/aleph.js +197 -0
- package/lib/backends/lighthouse.js +325 -0
- package/lib/backends/memory.js +140 -0
- package/lib/backends/pinata.js +330 -0
- package/lib/backends/resolve.js +72 -0
- package/lib/backends/storacha.js +178 -0
- package/lib/backends/types.js +187 -0
- package/lib/backup-car.js +1286 -0
- package/lib/backup-helpers.js +87 -0
- package/lib/backup-metadata.js +40 -0
- package/lib/backup.js +249 -0
- package/lib/block-bytes.js +39 -0
- package/lib/car-storage.js +342 -0
- package/lib/courier-sync.js +886 -0
- package/lib/dehydrate.js +146 -0
- package/lib/extract-blocks.js +258 -0
- package/lib/gateway-fetch.js +116 -0
- package/lib/ipns-helpers.js +324 -0
- package/lib/logger.js +128 -0
- package/lib/memory-courier.js +105 -0
- package/lib/orbitdb-storacha-bridge.js +2789 -0
- package/lib/pointer-ipns.js +234 -0
- package/lib/restore-cid.js +302 -0
- package/lib/ucan-bridge.js +921 -0
- package/lib/utils.js +316 -0
- package/package.json +172 -0
|
@@ -0,0 +1,921 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OrbitDB Storage Bridge - UCAN Edition
|
|
3
|
+
*
|
|
4
|
+
* Enhanced version that supports UCAN-based authentication for Storacha,
|
|
5
|
+
* eliminating the need for traditional storachaKey/storachaProof credentials.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as Client from "@storacha/client";
|
|
9
|
+
import { StoreMemory } from "@storacha/client/stores/memory";
|
|
10
|
+
import { Signer } from "@storacha/client/principal/ed25519";
|
|
11
|
+
import * as Delegation from "@ucanto/core/delegation";
|
|
12
|
+
import * as Proof from "@storacha/client/proof";
|
|
13
|
+
import { CID } from "multiformats/cid";
|
|
14
|
+
import { promises as fs } from "fs";
|
|
15
|
+
import { EventEmitter } from "events";
|
|
16
|
+
import { logger } from "./logger.js";
|
|
17
|
+
|
|
18
|
+
// Import existing functions from the main bridge
|
|
19
|
+
import {
|
|
20
|
+
extractDatabaseBlocks,
|
|
21
|
+
convertStorachaCIDToOrbitDB,
|
|
22
|
+
downloadBlock,
|
|
23
|
+
analyzeBlocks,
|
|
24
|
+
clearStorachaSpace as originalClearStorachaSpace,
|
|
25
|
+
reconstructWithoutManifest,
|
|
26
|
+
} from "./orbitdb-storacha-bridge.js";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Find the correct manifest block by matching log entries to database IDs
|
|
30
|
+
* (Shared implementation with main bridge)
|
|
31
|
+
*
|
|
32
|
+
* @param {Object} analysis - Block analysis results
|
|
33
|
+
* @returns {Object|null} - Correct manifest block or null if not found
|
|
34
|
+
*/
|
|
35
|
+
function findCorrectManifest(analysis) {
|
|
36
|
+
logger.info("๐ฏ Finding correct manifest from log entries...");
|
|
37
|
+
|
|
38
|
+
if (analysis.manifestBlocks.length === 1) {
|
|
39
|
+
logger.info(" โ
Only one manifest found, using it");
|
|
40
|
+
return analysis.manifestBlocks[0];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (analysis.manifestBlocks.length === 0) {
|
|
44
|
+
logger.info(" โ No manifest blocks found");
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Extract database IDs from log entries
|
|
49
|
+
const databaseIds = new Set();
|
|
50
|
+
for (const logEntry of analysis.logEntryBlocks) {
|
|
51
|
+
if (logEntry.content && logEntry.content.id) {
|
|
52
|
+
// Extract manifest CID from database address like '/orbitdb/zdpu...'
|
|
53
|
+
const manifestCID = logEntry.content.id.replace("/orbitdb/", "");
|
|
54
|
+
databaseIds.add(manifestCID);
|
|
55
|
+
logger.info(
|
|
56
|
+
` ๐ Log entry references database: ${logEntry.content.id} (manifest: ${manifestCID})`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
logger.info(
|
|
62
|
+
` ๐ Found ${databaseIds.size} unique database ID(s) from ${analysis.logEntryBlocks.length} log entries`,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Find manifest that matches the most referenced database ID
|
|
66
|
+
const manifestCounts = new Map();
|
|
67
|
+
for (const manifestBlock of analysis.manifestBlocks) {
|
|
68
|
+
const count = databaseIds.has(manifestBlock.cid) ? 1 : 0;
|
|
69
|
+
manifestCounts.set(manifestBlock.cid, count);
|
|
70
|
+
logger.info(
|
|
71
|
+
` ๐ Manifest ${manifestBlock.cid}: ${count > 0 ? "MATCHES" : "no match"}`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Find the manifest with the highest count (most log entry references)
|
|
76
|
+
let bestManifest = null;
|
|
77
|
+
let bestCount = -1;
|
|
78
|
+
|
|
79
|
+
for (const [manifestCID, count] of manifestCounts) {
|
|
80
|
+
if (count > bestCount) {
|
|
81
|
+
bestCount = count;
|
|
82
|
+
bestManifest = analysis.manifestBlocks.find((m) => m.cid === manifestCID);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (bestManifest && bestCount > 0) {
|
|
87
|
+
logger.info(
|
|
88
|
+
` โ
Selected manifest: ${bestManifest.cid} (referenced by ${bestCount} log entries)`,
|
|
89
|
+
);
|
|
90
|
+
return bestManifest;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Fallback: if no manifest matches log entries, use the first one and warn
|
|
94
|
+
logger.warn(
|
|
95
|
+
" โ ๏ธ No manifest matched log entries, using first manifest as fallback",
|
|
96
|
+
);
|
|
97
|
+
return analysis.manifestBlocks[0];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Default configuration options for UCAN bridge
|
|
102
|
+
*/
|
|
103
|
+
const DEFAULT_UCAN_OPTIONS = {
|
|
104
|
+
timeout: 30000,
|
|
105
|
+
gateway: "https://w3s.link",
|
|
106
|
+
batchSize: 10,
|
|
107
|
+
maxConcurrency: 3,
|
|
108
|
+
// UCAN-specific options
|
|
109
|
+
ucanFile: undefined, // Path to UCAN CAR file
|
|
110
|
+
ucanToken: undefined, // Base64-encoded UCAN token
|
|
111
|
+
recipientKey: undefined, // Recipient identity private key (for delegation)
|
|
112
|
+
agentDID: undefined, // Agent DID for the client
|
|
113
|
+
spaceDID: undefined, // Target space DID
|
|
114
|
+
// Fallback options
|
|
115
|
+
fallbackDatabaseName: undefined,
|
|
116
|
+
forceFallback: false,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Load UCAN delegation from CAR file
|
|
121
|
+
*
|
|
122
|
+
* @param {string} ucanFilePath - Path to UCAN CAR file
|
|
123
|
+
* @returns {Promise<Object>} - Parsed UCAN delegation
|
|
124
|
+
*/
|
|
125
|
+
async function loadUCANFromFile(ucanFilePath) {
|
|
126
|
+
try {
|
|
127
|
+
logger.info(`๐ Loading UCAN from file: ${ucanFilePath}`);
|
|
128
|
+
|
|
129
|
+
const carBytes = await fs.readFile(ucanFilePath);
|
|
130
|
+
|
|
131
|
+
// Extract the delegation directly from CAR bytes (like SecretShare)
|
|
132
|
+
const delegation = await Delegation.extract(carBytes);
|
|
133
|
+
|
|
134
|
+
if (!delegation.ok) {
|
|
135
|
+
throw new Error("Failed to extract delegation from CAR file");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const extractedDelegation = delegation.ok;
|
|
139
|
+
|
|
140
|
+
logger.info(`โ
UCAN loaded successfully`);
|
|
141
|
+
logger.info(
|
|
142
|
+
` ๐ Capabilities: ${extractedDelegation.capabilities.map((cap) => cap.can).join(", ")}`,
|
|
143
|
+
);
|
|
144
|
+
logger.info(` ๐ฏ Audience: ${extractedDelegation.audience.did()}`);
|
|
145
|
+
logger.info(` ๐ Issuer: ${extractedDelegation.issuer.did()}`);
|
|
146
|
+
|
|
147
|
+
return extractedDelegation;
|
|
148
|
+
} catch (error) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`Failed to load UCAN from file ${ucanFilePath}: ${error.message}`, { cause: error },
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Load UCAN delegation from base64 token
|
|
157
|
+
*
|
|
158
|
+
* @param {string} ucanToken - Base64-encoded UCAN token
|
|
159
|
+
* @returns {Promise<Object>} - Parsed UCAN delegation
|
|
160
|
+
*/
|
|
161
|
+
async function loadUCANFromToken(ucanToken) {
|
|
162
|
+
try {
|
|
163
|
+
logger.info(`๐ Loading UCAN from token`);
|
|
164
|
+
|
|
165
|
+
// Clean and normalize the token
|
|
166
|
+
// Remove whitespace, newlines, and handle URL-safe base64
|
|
167
|
+
let cleanedToken = ucanToken.trim().replace(/\s+/g, "");
|
|
168
|
+
|
|
169
|
+
// Handle URL-safe base64 (replace - with + and _ with /)
|
|
170
|
+
cleanedToken = cleanedToken.replace(/-/g, "+").replace(/_/g, "/");
|
|
171
|
+
|
|
172
|
+
// Add padding if needed (base64 strings should be multiples of 4)
|
|
173
|
+
while (cleanedToken.length % 4 !== 0) {
|
|
174
|
+
cleanedToken += "=";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Decode base64 token
|
|
178
|
+
let tokenBytes;
|
|
179
|
+
try {
|
|
180
|
+
tokenBytes = Buffer.from(cleanedToken, "base64");
|
|
181
|
+
} catch (base64Error) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`Invalid base64 token: ${base64Error.message}. Token preview: ${cleanedToken.substring(0, 50)}...`, { cause: base64Error },
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Try @ucanto/core/delegation first (for pure UCAN tokens)
|
|
188
|
+
try {
|
|
189
|
+
const delegation = await Delegation.extract(tokenBytes);
|
|
190
|
+
|
|
191
|
+
if (delegation.ok) {
|
|
192
|
+
const extractedDelegation = delegation.ok;
|
|
193
|
+
logger.info(`โ
UCAN token loaded successfully with @ucanto/core`);
|
|
194
|
+
logger.info(
|
|
195
|
+
` ๐ Capabilities: ${extractedDelegation.capabilities.map((cap) => cap.can).join(", ")}`,
|
|
196
|
+
);
|
|
197
|
+
logger.info(` ๐ฏ Audience: ${extractedDelegation.audience.did()}`);
|
|
198
|
+
logger.info(` ๐ Issuer: ${extractedDelegation.issuer.did()}`);
|
|
199
|
+
return extractedDelegation;
|
|
200
|
+
}
|
|
201
|
+
} catch (ucantoError) {
|
|
202
|
+
logger.info(
|
|
203
|
+
` โ ๏ธ @ucanto/core failed: ${ucantoError.message.substring(0, 100)}...`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Try @web3-storage/w3up-client/proof (for w3 CLI generated tokens)
|
|
208
|
+
try {
|
|
209
|
+
const proof = await Proof.parse(cleanedToken);
|
|
210
|
+
logger.info(`โ
UCAN token loaded successfully with w3up-client/proof`);
|
|
211
|
+
logger.info(` ๐ Proof CID: ${proof.cid}`);
|
|
212
|
+
|
|
213
|
+
// The proof itself is the delegation we need
|
|
214
|
+
return proof;
|
|
215
|
+
} catch (proofError) {
|
|
216
|
+
logger.info(
|
|
217
|
+
` โ ๏ธ w3up-client/proof failed: ${proofError.message.substring(0, 100)}...`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
throw new Error(
|
|
222
|
+
"Failed to parse token with both @ucanto/core and @web3-storage/w3up-client methods",
|
|
223
|
+
);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
throw new Error(`Failed to load UCAN from token: ${error.message}`, { cause: error });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Initialize Storacha client using UCAN delegation (SecretShare pattern)
|
|
231
|
+
*
|
|
232
|
+
* @param {Object} options - UCAN configuration options
|
|
233
|
+
* @returns {Promise<Object>} - Initialized Storacha client
|
|
234
|
+
*/
|
|
235
|
+
async function initializeStorachaClientWithUCAN(options) {
|
|
236
|
+
const config = { ...DEFAULT_UCAN_OPTIONS, ...options };
|
|
237
|
+
|
|
238
|
+
logger.info("๐ Initializing Storacha client with UCAN authentication...");
|
|
239
|
+
|
|
240
|
+
// Method 1: Use proper UCAN delegation (from our working example)
|
|
241
|
+
if (config.ucanToken && config.recipientKey) {
|
|
242
|
+
logger.info(" ๐ Using proper UCAN delegation approach...");
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
// Parse recipient identity from JSON archive and fix the key format
|
|
246
|
+
const recipientKeyData = JSON.parse(config.recipientKey);
|
|
247
|
+
|
|
248
|
+
// JSON serialization converts Uint8Array to plain object, so we need to reconstruct it
|
|
249
|
+
const fixedArchive = {
|
|
250
|
+
id: recipientKeyData.id,
|
|
251
|
+
keys: {
|
|
252
|
+
[recipientKeyData.id]: new Uint8Array(
|
|
253
|
+
Object.values(recipientKeyData.keys[recipientKeyData.id]),
|
|
254
|
+
),
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const recipientPrincipal = Signer.from(fixedArchive);
|
|
259
|
+
const store = new StoreMemory();
|
|
260
|
+
const client = await Client.create({
|
|
261
|
+
principal: recipientPrincipal,
|
|
262
|
+
store,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Parse delegation token
|
|
266
|
+
const delegationBytes = Buffer.from(config.ucanToken, "base64");
|
|
267
|
+
const delegation = await Delegation.extract(delegationBytes);
|
|
268
|
+
|
|
269
|
+
if (!delegation.ok) {
|
|
270
|
+
throw new Error("Failed to extract delegation from token");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (typeof client.addProof === "function") {
|
|
274
|
+
await client.addProof(delegation.ok);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Add space using delegation
|
|
278
|
+
const space = await client.addSpace(delegation.ok);
|
|
279
|
+
await client.setCurrentSpace(space.did());
|
|
280
|
+
|
|
281
|
+
logger.info("โ
Storacha client initialized with UCAN delegation");
|
|
282
|
+
logger.info(` ๐ค Agent: ${recipientPrincipal.did()}`);
|
|
283
|
+
logger.info(` ๐ Space: ${space.did()}`);
|
|
284
|
+
logger.info(` ๐ Capabilities: ${delegation.ok.capabilities.length}`);
|
|
285
|
+
|
|
286
|
+
return client;
|
|
287
|
+
} catch (error) {
|
|
288
|
+
logger.info(` โ ๏ธ UCAN delegation failed: ${error.message}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Method 2: Try legacy approaches as fallback
|
|
293
|
+
try {
|
|
294
|
+
let delegation;
|
|
295
|
+
|
|
296
|
+
if (config.ucanFile) {
|
|
297
|
+
delegation = await loadUCANFromFile(config.ucanFile);
|
|
298
|
+
} else if (config.ucanToken) {
|
|
299
|
+
delegation = await loadUCANFromToken(config.ucanToken);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (delegation && delegation.audience) {
|
|
303
|
+
logger.info(` ๐ Using legacy delegation approach...`);
|
|
304
|
+
|
|
305
|
+
const audience = delegation.audience;
|
|
306
|
+
const store = new StoreMemory();
|
|
307
|
+
const client = await Client.create({ principal: audience, store });
|
|
308
|
+
if (typeof client.addProof === "function") {
|
|
309
|
+
await client.addProof(delegation);
|
|
310
|
+
}
|
|
311
|
+
const space = await client.addSpace(delegation);
|
|
312
|
+
await client.setCurrentSpace(space.did());
|
|
313
|
+
|
|
314
|
+
logger.info("โ
Storacha client initialized with legacy delegation");
|
|
315
|
+
logger.info(` ๐ค Agent: ${audience.did()}`);
|
|
316
|
+
logger.info(` ๐ Space: ${space.did()}`);
|
|
317
|
+
|
|
318
|
+
return client;
|
|
319
|
+
}
|
|
320
|
+
} catch (delegationError) {
|
|
321
|
+
logger.info(` โ ๏ธ Legacy delegation failed: ${delegationError.message}`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
throw new Error(
|
|
325
|
+
"All UCAN authentication methods failed. Please check your UCAN credentials.",
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Backup database using UCAN authentication
|
|
331
|
+
*
|
|
332
|
+
* @param {Object} orbitdb - OrbitDB instance
|
|
333
|
+
* @param {string} databaseAddress - Database address or name
|
|
334
|
+
* @param {Object} options - UCAN backup options
|
|
335
|
+
* @returns {Promise<Object>} - Backup result
|
|
336
|
+
*/
|
|
337
|
+
export async function backupDatabaseWithUCAN(
|
|
338
|
+
orbitdb,
|
|
339
|
+
databaseAddress,
|
|
340
|
+
options = {},
|
|
341
|
+
) {
|
|
342
|
+
const config = { ...DEFAULT_UCAN_OPTIONS, ...options };
|
|
343
|
+
const eventEmitter = options.eventEmitter;
|
|
344
|
+
|
|
345
|
+
logger.info("๐ Starting OrbitDB Database Backup to Storacha (UCAN)");
|
|
346
|
+
logger.info(`๐ Database: ${databaseAddress}`);
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
// Initialize Storacha client with UCAN
|
|
350
|
+
const client = await initializeStorachaClientWithUCAN(config);
|
|
351
|
+
|
|
352
|
+
// Open the database
|
|
353
|
+
const database =
|
|
354
|
+
typeof databaseAddress === "string" &&
|
|
355
|
+
databaseAddress.startsWith("/orbitdb/")
|
|
356
|
+
? await orbitdb.open(databaseAddress)
|
|
357
|
+
: await orbitdb.open(databaseAddress);
|
|
358
|
+
|
|
359
|
+
// Extract all blocks (reuse existing function)
|
|
360
|
+
const { blocks, blockSources, manifestCID } =
|
|
361
|
+
await extractDatabaseBlocks(database);
|
|
362
|
+
|
|
363
|
+
// Upload blocks to Storacha with progress tracking
|
|
364
|
+
const { successful, cidMappings } = await uploadBlocksToStorachaUCAN(
|
|
365
|
+
blocks,
|
|
366
|
+
client,
|
|
367
|
+
config.batchSize,
|
|
368
|
+
config.maxConcurrency,
|
|
369
|
+
eventEmitter,
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
if (successful.length === 0) {
|
|
373
|
+
throw new Error("No blocks were successfully uploaded");
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Get block summary
|
|
377
|
+
const blockSummary = {};
|
|
378
|
+
for (const [_hash, source] of blockSources) {
|
|
379
|
+
blockSummary[source] = (blockSummary[source] || 0) + 1;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
logger.info("โ
UCAN Backup completed successfully!");
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
success: true,
|
|
386
|
+
manifestCID,
|
|
387
|
+
databaseAddress: database.address,
|
|
388
|
+
databaseName: database.name,
|
|
389
|
+
blocksTotal: blocks.size,
|
|
390
|
+
blocksUploaded: successful.length,
|
|
391
|
+
blockSummary,
|
|
392
|
+
cidMappings: Object.fromEntries(cidMappings),
|
|
393
|
+
};
|
|
394
|
+
} catch (error) {
|
|
395
|
+
logger.error("โ UCAN Backup failed:", error.message);
|
|
396
|
+
return {
|
|
397
|
+
success: false,
|
|
398
|
+
error: error.message,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Upload blocks to Storacha using UCAN client
|
|
405
|
+
*/
|
|
406
|
+
async function uploadBlocksToStorachaUCAN(
|
|
407
|
+
blocks,
|
|
408
|
+
client,
|
|
409
|
+
batchSize = 10,
|
|
410
|
+
maxConcurrency = 3,
|
|
411
|
+
eventEmitter = null,
|
|
412
|
+
) {
|
|
413
|
+
logger.info(
|
|
414
|
+
`๐ค Uploading ${blocks.size} blocks to Storacha (UCAN) in batches of ${batchSize}...`,
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
const uploadResults = [];
|
|
418
|
+
const cidMappings = new Map();
|
|
419
|
+
const blocksArray = Array.from(blocks.entries());
|
|
420
|
+
const totalBlocks = blocks.size;
|
|
421
|
+
let completedBlocks = 0;
|
|
422
|
+
|
|
423
|
+
// Emit initial progress
|
|
424
|
+
if (eventEmitter) {
|
|
425
|
+
eventEmitter.emit("uploadProgress", {
|
|
426
|
+
type: "upload",
|
|
427
|
+
current: 0,
|
|
428
|
+
total: totalBlocks,
|
|
429
|
+
percentage: 0,
|
|
430
|
+
status: "starting",
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Helper function to upload a single block
|
|
435
|
+
const uploadSingleBlock = async ([hash, blockData]) => {
|
|
436
|
+
try {
|
|
437
|
+
const blockFile = new File([blockData.bytes], hash, {
|
|
438
|
+
type: "application/octet-stream",
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
logger.info(
|
|
442
|
+
` ๐ค Uploading block ${hash} (${blockData.bytes.length} bytes)...`,
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
// Use UCAN-authenticated client
|
|
446
|
+
const result = await client.uploadFile(blockFile);
|
|
447
|
+
const uploadedCID = result.toString();
|
|
448
|
+
|
|
449
|
+
logger.info(` โ
Uploaded (UCAN): ${hash} โ ${uploadedCID}`);
|
|
450
|
+
|
|
451
|
+
// Update progress
|
|
452
|
+
completedBlocks++;
|
|
453
|
+
if (eventEmitter) {
|
|
454
|
+
eventEmitter.emit("uploadProgress", {
|
|
455
|
+
type: "upload",
|
|
456
|
+
current: completedBlocks,
|
|
457
|
+
total: totalBlocks,
|
|
458
|
+
percentage: Math.round((completedBlocks / totalBlocks) * 100),
|
|
459
|
+
status: "uploading",
|
|
460
|
+
currentBlock: {
|
|
461
|
+
hash,
|
|
462
|
+
uploadedCID,
|
|
463
|
+
size: blockData.bytes.length,
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
originalHash: hash,
|
|
470
|
+
uploadedCID,
|
|
471
|
+
size: blockData.bytes.length,
|
|
472
|
+
};
|
|
473
|
+
} catch (error) {
|
|
474
|
+
logger.error(` โ Failed to upload block ${hash}: ${error.message}`);
|
|
475
|
+
|
|
476
|
+
completedBlocks++;
|
|
477
|
+
if (eventEmitter) {
|
|
478
|
+
eventEmitter.emit("uploadProgress", {
|
|
479
|
+
type: "upload",
|
|
480
|
+
current: completedBlocks,
|
|
481
|
+
total: totalBlocks,
|
|
482
|
+
percentage: Math.round((completedBlocks / totalBlocks) * 100),
|
|
483
|
+
status: "uploading",
|
|
484
|
+
error: {
|
|
485
|
+
hash,
|
|
486
|
+
message: error.message,
|
|
487
|
+
},
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
originalHash: hash,
|
|
493
|
+
error: error.message,
|
|
494
|
+
size: blockData.bytes.length,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
// Process blocks in batches (reuse existing batching logic)
|
|
500
|
+
for (let i = 0; i < blocksArray.length; i += batchSize * maxConcurrency) {
|
|
501
|
+
const megaBatch = blocksArray.slice(i, i + batchSize * maxConcurrency);
|
|
502
|
+
const batches = [];
|
|
503
|
+
|
|
504
|
+
for (let j = 0; j < megaBatch.length; j += batchSize) {
|
|
505
|
+
const batch = megaBatch.slice(j, j + batchSize);
|
|
506
|
+
batches.push(batch);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
logger.info(
|
|
510
|
+
` ๐ Processing ${batches.length} concurrent batches (${megaBatch.length} blocks)...`,
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
const batchPromises = batches.map(async (batch, batchIndex) => {
|
|
514
|
+
logger.info(
|
|
515
|
+
` ๐ฆ Batch ${batchIndex + 1}/${batches.length}: ${batch.length} blocks`,
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
const batchResults = await Promise.allSettled(
|
|
519
|
+
batch.map(uploadSingleBlock),
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
return batchResults.map((result) =>
|
|
523
|
+
result.status === "fulfilled"
|
|
524
|
+
? result.value
|
|
525
|
+
: {
|
|
526
|
+
originalHash: "unknown",
|
|
527
|
+
error: result.reason?.message || "Unknown error",
|
|
528
|
+
size: 0,
|
|
529
|
+
},
|
|
530
|
+
);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
const batchResults = await Promise.all(batchPromises);
|
|
534
|
+
|
|
535
|
+
for (const batchResult of batchResults) {
|
|
536
|
+
for (const result of batchResult) {
|
|
537
|
+
uploadResults.push(result);
|
|
538
|
+
if (result.uploadedCID) {
|
|
539
|
+
cidMappings.set(result.originalHash, result.uploadedCID);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const successful = uploadResults.filter((r) => r.uploadedCID);
|
|
546
|
+
const failed = uploadResults.filter((r) => r.error);
|
|
547
|
+
|
|
548
|
+
logger.info(` ๐ UCAN Upload summary:`);
|
|
549
|
+
logger.info(` Total blocks: ${blocks.size}`);
|
|
550
|
+
logger.info(` Successful: ${successful.length}`);
|
|
551
|
+
logger.info(` Failed: ${failed.length}`);
|
|
552
|
+
|
|
553
|
+
if (eventEmitter) {
|
|
554
|
+
eventEmitter.emit("uploadProgress", {
|
|
555
|
+
type: "upload",
|
|
556
|
+
current: totalBlocks,
|
|
557
|
+
total: totalBlocks,
|
|
558
|
+
percentage: 100,
|
|
559
|
+
status: "completed",
|
|
560
|
+
summary: {
|
|
561
|
+
successful: successful.length,
|
|
562
|
+
failed: failed.length,
|
|
563
|
+
},
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return { uploadResults, successful, failed, cidMappings };
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Restore database from Storacha using UCAN authentication
|
|
572
|
+
*
|
|
573
|
+
* @param {Object} orbitdb - Target OrbitDB instance
|
|
574
|
+
* @param {Object} options - UCAN restore options
|
|
575
|
+
* @returns {Promise<Object>} - Restore result
|
|
576
|
+
*/
|
|
577
|
+
export async function restoreDatabaseFromSpaceWithUCAN(orbitdb, options = {}) {
|
|
578
|
+
const config = { ...DEFAULT_UCAN_OPTIONS, ...options };
|
|
579
|
+
const eventEmitter = options.eventEmitter;
|
|
580
|
+
|
|
581
|
+
logger.info("๐ Starting OrbitDB Restore from Storacha (UCAN)");
|
|
582
|
+
|
|
583
|
+
try {
|
|
584
|
+
// Step 1: List ALL files in Storacha space using UCAN
|
|
585
|
+
logger.info(
|
|
586
|
+
"\\n๐ Step 1: Discovering all files in Storacha space (UCAN)...",
|
|
587
|
+
);
|
|
588
|
+
const spaceFiles = await listStorachaSpaceFilesWithUCAN(config);
|
|
589
|
+
|
|
590
|
+
if (spaceFiles.length === 0) {
|
|
591
|
+
throw new Error("No files found in Storacha space");
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
logger.info(
|
|
595
|
+
` ๐ SUCCESS! Found ${spaceFiles.length} files in space using UCAN authentication`,
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
// Step 2: Download ALL files from space with progress tracking
|
|
599
|
+
const downloadedBlocks = await downloadBlocksWithProgressUCAN(
|
|
600
|
+
spaceFiles,
|
|
601
|
+
orbitdb,
|
|
602
|
+
config,
|
|
603
|
+
eventEmitter,
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
// Step 3: Intelligent block analysis
|
|
607
|
+
logger.info(
|
|
608
|
+
"\\n๐ Step 3: Analyzing block structure with advanced intelligence...",
|
|
609
|
+
);
|
|
610
|
+
const analysis = await analyzeBlocks(
|
|
611
|
+
orbitdb.ipfs.blockstore,
|
|
612
|
+
downloadedBlocks,
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
if (analysis.manifestBlocks.length === 0 || options.forceFallback) {
|
|
616
|
+
logger.info(
|
|
617
|
+
"โ ๏ธ No manifest blocks found - attempting fallback reconstruction...",
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
// Use existing fallback reconstruction logic
|
|
621
|
+
const fallbackResult = await reconstructWithoutManifest(
|
|
622
|
+
orbitdb,
|
|
623
|
+
downloadedBlocks,
|
|
624
|
+
config,
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
return {
|
|
628
|
+
database: fallbackResult.database,
|
|
629
|
+
metadata: fallbackResult.metadata,
|
|
630
|
+
entriesCount: fallbackResult.entriesCount,
|
|
631
|
+
entriesRecovered: fallbackResult.entriesCount,
|
|
632
|
+
method: "fallback-reconstruction",
|
|
633
|
+
success: true,
|
|
634
|
+
preservedHashes: false,
|
|
635
|
+
preservedAddress: false,
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Step 4: Reconstruct database using discovered manifest
|
|
640
|
+
logger.info("\\n๐ Step 4: Reconstructing database from analysis...");
|
|
641
|
+
|
|
642
|
+
// Find the correct manifest by matching log entries to database IDs
|
|
643
|
+
const correctManifest = findCorrectManifest(analysis);
|
|
644
|
+
if (!correctManifest) {
|
|
645
|
+
throw new Error("Could not determine correct manifest from log entries");
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const databaseAddress = `/orbitdb/${correctManifest.cid}`;
|
|
649
|
+
|
|
650
|
+
logger.info(` ๐ฅ Opening database at: ${databaseAddress}`);
|
|
651
|
+
logger.info(
|
|
652
|
+
` ๐ฏ Selected manifest: ${correctManifest.cid} (matched from log entries)`,
|
|
653
|
+
);
|
|
654
|
+
const reconstructedDB = await orbitdb.open(databaseAddress);
|
|
655
|
+
|
|
656
|
+
// Wait for entries to load
|
|
657
|
+
logger.info(" โณ Waiting for entries to load...");
|
|
658
|
+
await new Promise((resolve) => setTimeout(resolve, config.timeout / 10));
|
|
659
|
+
const reconstructedEntries = await reconstructedDB.all();
|
|
660
|
+
await new Promise((resolve) => setTimeout(resolve, config.timeout / 10));
|
|
661
|
+
|
|
662
|
+
// Handle different database types properly (reuse existing logic)
|
|
663
|
+
let entriesArray;
|
|
664
|
+
let entriesCount;
|
|
665
|
+
if (reconstructedDB.type === "keyvalue") {
|
|
666
|
+
const logEntries = await reconstructedDB.log.values();
|
|
667
|
+
entriesArray = logEntries.map((logEntry) => ({
|
|
668
|
+
hash: logEntry.hash,
|
|
669
|
+
payload: logEntry.payload,
|
|
670
|
+
}));
|
|
671
|
+
entriesCount = Object.keys(reconstructedEntries).length;
|
|
672
|
+
} else {
|
|
673
|
+
entriesArray = Array.isArray(reconstructedEntries)
|
|
674
|
+
? reconstructedEntries
|
|
675
|
+
: [];
|
|
676
|
+
entriesCount = entriesArray.length;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
logger.info(` ๐ Reconstructed entries: ${entriesCount}`);
|
|
680
|
+
logger.info(` ๐ Database type: ${reconstructedDB.type}`);
|
|
681
|
+
|
|
682
|
+
logger.info("โ
UCAN Restore completed successfully!");
|
|
683
|
+
|
|
684
|
+
return {
|
|
685
|
+
success: true,
|
|
686
|
+
database: reconstructedDB,
|
|
687
|
+
orbitdb: orbitdb,
|
|
688
|
+
manifestCID: correctManifest.cid,
|
|
689
|
+
address: reconstructedDB.address,
|
|
690
|
+
name: reconstructedDB.name,
|
|
691
|
+
type: reconstructedDB.type,
|
|
692
|
+
entriesRecovered: entriesCount,
|
|
693
|
+
blocksRestored: downloadedBlocks.size,
|
|
694
|
+
addressMatch: reconstructedDB.address === databaseAddress,
|
|
695
|
+
spaceFilesFound: spaceFiles.length,
|
|
696
|
+
analysis,
|
|
697
|
+
entries: entriesArray,
|
|
698
|
+
};
|
|
699
|
+
} catch (error) {
|
|
700
|
+
logger.error("โ UCAN Restore failed:", error.message);
|
|
701
|
+
|
|
702
|
+
return {
|
|
703
|
+
success: false,
|
|
704
|
+
error: error.message,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* List Storacha space files using UCAN authentication
|
|
711
|
+
*/
|
|
712
|
+
async function listStorachaSpaceFilesWithUCAN(options = {}) {
|
|
713
|
+
logger.info("๐ Listing files in Storacha space using UCAN...");
|
|
714
|
+
|
|
715
|
+
try {
|
|
716
|
+
// Initialize client with UCAN
|
|
717
|
+
const client = await initializeStorachaClientWithUCAN(options);
|
|
718
|
+
|
|
719
|
+
// Use existing list logic but with UCAN client
|
|
720
|
+
const listOptions = {};
|
|
721
|
+
if (options.size) {
|
|
722
|
+
listOptions.size = parseInt(String(options.size));
|
|
723
|
+
} else {
|
|
724
|
+
listOptions.size = 1000000;
|
|
725
|
+
}
|
|
726
|
+
if (options.cursor) {
|
|
727
|
+
listOptions.cursor = options.cursor;
|
|
728
|
+
}
|
|
729
|
+
if (options.pre) {
|
|
730
|
+
listOptions.pre = options.pre;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const result = await client.capability.upload.list(listOptions);
|
|
734
|
+
|
|
735
|
+
logger.info(` โ
Found ${result.results.length} uploads in space (UCAN)`);
|
|
736
|
+
|
|
737
|
+
const spaceFiles = result.results.map((upload) => ({
|
|
738
|
+
root: upload.root.toString(),
|
|
739
|
+
uploaded: upload.insertedAt ? new Date(upload.insertedAt) : new Date(),
|
|
740
|
+
size:
|
|
741
|
+
upload.shards?.reduce((total, shard) => {
|
|
742
|
+
return total + (shard.size || 0);
|
|
743
|
+
}, 0) || "unknown",
|
|
744
|
+
shards: upload.shards?.length || 0,
|
|
745
|
+
insertedAt: upload.insertedAt,
|
|
746
|
+
updatedAt: upload.updatedAt,
|
|
747
|
+
}));
|
|
748
|
+
|
|
749
|
+
return spaceFiles;
|
|
750
|
+
} catch (error) {
|
|
751
|
+
logger.error(" โ UCAN listing error:", error.message);
|
|
752
|
+
throw error;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Download blocks with UCAN authentication
|
|
758
|
+
*/
|
|
759
|
+
async function downloadBlocksWithProgressUCAN(
|
|
760
|
+
spaceFiles,
|
|
761
|
+
currentOrbitDB,
|
|
762
|
+
config,
|
|
763
|
+
eventEmitter = null,
|
|
764
|
+
) {
|
|
765
|
+
logger.info("\\n๐ฅ Downloading all space files (UCAN)...");
|
|
766
|
+
const downloadedBlocks = new Map();
|
|
767
|
+
const totalFiles = spaceFiles.length;
|
|
768
|
+
let completedFiles = 0;
|
|
769
|
+
|
|
770
|
+
if (eventEmitter) {
|
|
771
|
+
eventEmitter.emit("downloadProgress", {
|
|
772
|
+
type: "download",
|
|
773
|
+
current: 0,
|
|
774
|
+
total: totalFiles,
|
|
775
|
+
percentage: 0,
|
|
776
|
+
status: "starting",
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
for (const spaceFile of spaceFiles) {
|
|
781
|
+
const storachaCID = spaceFile.root;
|
|
782
|
+
logger.info(` ๐ Downloading (UCAN): ${storachaCID}`);
|
|
783
|
+
|
|
784
|
+
try {
|
|
785
|
+
const bytes = await downloadBlock(storachaCID, config);
|
|
786
|
+
|
|
787
|
+
const orbitdbCID = convertStorachaCIDToOrbitDB(storachaCID);
|
|
788
|
+
const parsedCID = CID.parse(orbitdbCID);
|
|
789
|
+
|
|
790
|
+
// Store in target blockstore with error handling for closed blockstore
|
|
791
|
+
if (!currentOrbitDB?.ipfs?.blockstore) {
|
|
792
|
+
throw new Error(
|
|
793
|
+
"Blockstore is not available - OrbitDB may have been closed",
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
try {
|
|
797
|
+
await currentOrbitDB.ipfs.blockstore.put(parsedCID, bytes);
|
|
798
|
+
} catch (error) {
|
|
799
|
+
if (
|
|
800
|
+
error.message?.includes("not open") ||
|
|
801
|
+
error.message?.includes("closed") ||
|
|
802
|
+
error.message?.includes("Database is not open")
|
|
803
|
+
) {
|
|
804
|
+
throw new Error(
|
|
805
|
+
`Blockstore operation failed - OrbitDB was closed during download: ${error.message}`, { cause: error },
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
throw error;
|
|
809
|
+
}
|
|
810
|
+
downloadedBlocks.set(orbitdbCID, { storachaCID, bytes: bytes.length });
|
|
811
|
+
|
|
812
|
+
logger.info(` โ
Stored (UCAN): ${orbitdbCID}`);
|
|
813
|
+
|
|
814
|
+
completedFiles++;
|
|
815
|
+
if (eventEmitter) {
|
|
816
|
+
eventEmitter.emit("downloadProgress", {
|
|
817
|
+
type: "download",
|
|
818
|
+
current: completedFiles,
|
|
819
|
+
total: totalFiles,
|
|
820
|
+
percentage: Math.round((completedFiles / totalFiles) * 100),
|
|
821
|
+
status: "downloading",
|
|
822
|
+
currentBlock: {
|
|
823
|
+
storachaCID,
|
|
824
|
+
orbitdbCID,
|
|
825
|
+
size: bytes.length,
|
|
826
|
+
},
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
} catch (error) {
|
|
830
|
+
logger.error(` โ Failed (UCAN): ${storachaCID} - ${error.message}`);
|
|
831
|
+
|
|
832
|
+
completedFiles++;
|
|
833
|
+
if (eventEmitter) {
|
|
834
|
+
eventEmitter.emit("downloadProgress", {
|
|
835
|
+
type: "download",
|
|
836
|
+
current: completedFiles,
|
|
837
|
+
total: totalFiles,
|
|
838
|
+
percentage: Math.round((completedFiles / totalFiles) * 100),
|
|
839
|
+
status: "downloading",
|
|
840
|
+
error: {
|
|
841
|
+
storachaCID,
|
|
842
|
+
message: error.message,
|
|
843
|
+
},
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (eventEmitter) {
|
|
850
|
+
eventEmitter.emit("downloadProgress", {
|
|
851
|
+
type: "download",
|
|
852
|
+
current: totalFiles,
|
|
853
|
+
total: totalFiles,
|
|
854
|
+
percentage: 100,
|
|
855
|
+
status: "completed",
|
|
856
|
+
summary: {
|
|
857
|
+
downloaded: downloadedBlocks.size,
|
|
858
|
+
failed: totalFiles - downloadedBlocks.size,
|
|
859
|
+
},
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
logger.info(` ๐ Downloaded ${downloadedBlocks.size} blocks total (UCAN)`);
|
|
864
|
+
return downloadedBlocks;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Clear Storacha space using UCAN authentication
|
|
869
|
+
*/
|
|
870
|
+
export async function clearStorachaSpaceWithUCAN(options = {}) {
|
|
871
|
+
logger.info("๐งน Clearing Storacha space using UCAN...");
|
|
872
|
+
|
|
873
|
+
// Initialize client with UCAN
|
|
874
|
+
const _client = await initializeStorachaClientWithUCAN(options); // Prefixed with underscore
|
|
875
|
+
|
|
876
|
+
// Use the existing clear logic but pass UCAN-authenticated client
|
|
877
|
+
// This would require modifying the original functions to accept a client parameter
|
|
878
|
+
return await originalClearStorachaSpace({
|
|
879
|
+
...options,
|
|
880
|
+
// Pass the client somehow - this needs refactoring in original functions
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Enhanced OrbitDBStorachaBridge class with UCAN support
|
|
886
|
+
*/
|
|
887
|
+
export class OrbitDBStorachaBridgeUCAN extends EventEmitter {
|
|
888
|
+
constructor(options = {}) {
|
|
889
|
+
super();
|
|
890
|
+
this.config = { ...DEFAULT_UCAN_OPTIONS, ...options };
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
async backup(orbitdb, databaseAddress, options = {}) {
|
|
894
|
+
return await backupDatabaseWithUCAN(orbitdb, databaseAddress, {
|
|
895
|
+
...this.config,
|
|
896
|
+
...options,
|
|
897
|
+
eventEmitter: this,
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
async restoreFromSpace(orbitdb, options = {}) {
|
|
902
|
+
return await restoreDatabaseFromSpaceWithUCAN(orbitdb, {
|
|
903
|
+
...this.config,
|
|
904
|
+
...options,
|
|
905
|
+
eventEmitter: this,
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Utility methods
|
|
910
|
+
async listSpaceFiles(options = {}) {
|
|
911
|
+
return await listStorachaSpaceFilesWithUCAN({ ...this.config, ...options });
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
async clearSpace(options = {}) {
|
|
915
|
+
return await clearStorachaSpaceWithUCAN({ ...this.config, ...options });
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
convertCID(storachaCID) {
|
|
919
|
+
return convertStorachaCIDToOrbitDB(storachaCID);
|
|
920
|
+
}
|
|
921
|
+
}
|