@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.
@@ -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
+ }