@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/lib/utils.js ADDED
@@ -0,0 +1,316 @@
1
+ /**
2
+ * OrbitDB Storage Bridge - Utility Functions
3
+ *
4
+ * Common utility functions for OrbitDB operations and cleanup
5
+ */
6
+
7
+ import { identify } from "@libp2p/identify";
8
+ import { noise } from "@chainsafe/libp2p-noise";
9
+ import { yamux } from "@chainsafe/libp2p-yamux";
10
+ import { tcp } from "@libp2p/tcp";
11
+ import { bootstrap } from "@libp2p/bootstrap";
12
+ import { gossipsub } from "@libp2p/gossipsub";
13
+ import { kadDHT } from "@libp2p/kad-dht";
14
+ import { ping } from "@libp2p/ping";
15
+ import { createHelia } from "helia";
16
+ import { unixfs } from "@helia/unixfs";
17
+ import { createOrbitDB } from "@orbitdb/core";
18
+ import { LevelBlockstore } from "blockstore-level";
19
+ import { LevelDatastore } from "datastore-level";
20
+ import { logger, createLogger } from "./logger.js";
21
+
22
+ const HELIA_LOGGER = createLogger("helia");
23
+ const HELIA_COLOR = "\x1b[95m";
24
+ const COLOR_RESET = "\x1b[0m";
25
+
26
+ function logHelia(message) {
27
+ HELIA_LOGGER(`${HELIA_COLOR}${message}${COLOR_RESET}`);
28
+ }
29
+
30
+ function attachHeliaLogging(libp2p, label) {
31
+ const prefix = label ? `[${label}] ` : "";
32
+ const safePeer = (peer) =>
33
+ typeof peer?.toString === "function" ? peer.toString() : `${peer ?? ""}`;
34
+
35
+ libp2p.addEventListener("peer:connect", (evt) => {
36
+ logHelia(`${prefix}peer:connect ${safePeer(evt?.detail)}`);
37
+ });
38
+ libp2p.addEventListener("peer:disconnect", (evt) => {
39
+ logHelia(`${prefix}peer:disconnect ${safePeer(evt?.detail)}`);
40
+ });
41
+ libp2p.addEventListener("peer:discovery", (evt) => {
42
+ logHelia(`${prefix}peer:discovery ${safePeer(evt?.detail?.id)}`);
43
+ });
44
+ libp2p.addEventListener("connection:open", (evt) => {
45
+ logHelia(`${prefix}connection:open ${safePeer(evt?.detail?.remotePeer)}`);
46
+ });
47
+ libp2p.addEventListener("connection:close", (evt) => {
48
+ logHelia(`${prefix}connection:close ${safePeer(evt?.detail?.remotePeer)}`);
49
+ });
50
+ }
51
+
52
+ /**
53
+ * Bootstrap nodes for connecting to the public IPFS network
54
+ * Based on working implementation from commit 556736f
55
+ */
56
+ const BOOTSTRAP_NODES = [
57
+ // Official IPFS bootstrap nodes
58
+ "/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
59
+ "/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa",
60
+ "/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb",
61
+ "/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt",
62
+ // Direct IP bootstrap (more reliable in restricted networks)
63
+ "/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ",
64
+ "/ip4/104.131.131.82/udp/4001/quic-v1/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ",
65
+ ];
66
+
67
+ /**
68
+ * Clean up OrbitDB directories
69
+ */
70
+ export async function cleanupOrbitDBDirectories() {
71
+ const fs = await import("fs");
72
+
73
+ try {
74
+ const entries = await fs.promises.readdir(".", { withFileTypes: true });
75
+ const orbitdbDirs = entries.filter(
76
+ (entry) =>
77
+ entry.isDirectory() &&
78
+ (entry.name.startsWith("orbitdb-bridge-") ||
79
+ entry.name.includes("orbitdb-bridge-")),
80
+ );
81
+
82
+ for (const dir of orbitdbDirs) {
83
+ try {
84
+ await fs.promises.rm(dir.name, { recursive: true, force: true });
85
+ logger.info(`Cleaned up: ${dir.name}`);
86
+ logger.info({ directory: dir.name }, `🧹 Cleaned up: ${dir.name}`);
87
+ } catch (error) {
88
+ logger.warn(`Could not clean up ${dir.name}: ${error.message}`);
89
+ }
90
+ }
91
+
92
+ if (orbitdbDirs.length === 0) {
93
+ logger.info("🧹 No OrbitDB directories to clean up");
94
+ } else {
95
+ logger.info(`Cleaned up ${orbitdbDirs.length} OrbitDB directories`);
96
+ }
97
+ } catch (error) {
98
+ logger.warn(`Cleanup warning: ${error.message}`);
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Clean up all test-related directories and CAR files
104
+ * This function removes all common test artifacts created during testing
105
+ */
106
+ export async function cleanupAllTestArtifacts() {
107
+ const fs = await import("fs");
108
+ // const path = await import('path') // Currently unused
109
+
110
+ logger.info("🧹 Starting comprehensive test cleanup...");
111
+
112
+ // Test directories to clean up
113
+ const testDirectories = [
114
+ "./test-car-storage-bridge",
115
+ "./test-orbitdb-car-integration",
116
+ "./test-preservation",
117
+ "./test-advanced",
118
+ "./test-data",
119
+ "./test-hash-preservation",
120
+ "./orbitdb-todo-restored",
121
+ "./orbitdb-todo-source",
122
+ "./helia-car-demo",
123
+ "./storage-demo",
124
+ ];
125
+
126
+ // CAR files to clean up (common patterns)
127
+ // const carFilePatterns = [
128
+ // 'test-*.car',
129
+ // '*-test.car',
130
+ // 'todos-backup.car',
131
+ // 'storacha-hash-preservation-test.car'
132
+ // ]
133
+
134
+ let cleanedDirs = 0;
135
+ let cleanedFiles = 0;
136
+
137
+ // Clean up test directories
138
+ for (const dir of testDirectories) {
139
+ try {
140
+ await fs.promises.rm(dir, { recursive: true, force: true });
141
+ logger.info(`Removed test directory: ${dir}`);
142
+ cleanedDirs++;
143
+ } catch (error) {
144
+ if (error.code !== "ENOENT") {
145
+ logger.warn(`Could not remove ${dir}: ${error.message}`);
146
+ }
147
+ }
148
+ }
149
+
150
+ // Clean up CAR files
151
+ try {
152
+ const entries = await fs.promises.readdir(".", { withFileTypes: true });
153
+ const carFiles = entries.filter(
154
+ (entry) => entry.isFile() && entry.name.endsWith(".car"),
155
+ );
156
+
157
+ for (const carFile of carFiles) {
158
+ try {
159
+ await fs.promises.unlink(carFile.name);
160
+ logger.info(`Removed CAR file: ${carFile.name}`);
161
+ cleanedFiles++;
162
+ } catch (error) {
163
+ logger.warn(`Could not remove ${carFile.name}: ${error.message}`);
164
+ }
165
+ }
166
+ } catch (error) {
167
+ logger.warn(`Error scanning for CAR files: ${error.message}`);
168
+ }
169
+
170
+ // Also clean up OrbitDB directories
171
+ await cleanupOrbitDBDirectories();
172
+
173
+ logger.info(
174
+ `Comprehensive cleanup completed: ${cleanedDirs} directories, ${cleanedFiles} CAR files`,
175
+ );
176
+ }
177
+
178
+ /**
179
+ * Clean up specific test directory and associated CAR files
180
+ * @param {string} testDir - The test directory path
181
+ * @param {string} [carPrefix] - Optional prefix for CAR files to clean
182
+ */
183
+ export async function cleanupTestDirectory(testDir, carPrefix = "") {
184
+ const fs = await import("fs");
185
+
186
+ try {
187
+ // Remove test directory
188
+ await fs.promises.rm(testDir, { recursive: true, force: true });
189
+ logger.info(`Removed test directory: ${testDir}`);
190
+
191
+ // Remove associated CAR files if prefix provided
192
+ if (carPrefix) {
193
+ try {
194
+ const entries = await fs.promises.readdir(".", { withFileTypes: true });
195
+ const carFiles = entries.filter(
196
+ (entry) =>
197
+ entry.isFile() &&
198
+ entry.name.endsWith(".car") &&
199
+ entry.name.includes(carPrefix),
200
+ );
201
+
202
+ for (const carFile of carFiles) {
203
+ await fs.promises.unlink(carFile.name);
204
+ logger.info(`Removed CAR file: ${carFile.name}`);
205
+ }
206
+ } catch (error) {
207
+ logger.warn(
208
+ `Error cleaning CAR files with prefix ${carPrefix}: ${error.message}`,
209
+ );
210
+ }
211
+ }
212
+ } catch (error) {
213
+ if (error.code !== "ENOENT") {
214
+ logger.warn(
215
+ `Could not clean up test directory ${testDir}: ${error.message}`,
216
+ );
217
+ }
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Create a Helia/OrbitDB instance with specified suffix
223
+ * Enhanced with bootstrap nodes for connecting to the public IPFS network
224
+ * @param {string} suffix - Suffix for directory names
225
+ * @param {Object} options - Configuration options
226
+ * @param {boolean} options.dhtClientMode - If true, use DHT in client-only mode (default: true).
227
+ * Client mode works better behind NAT. Set to false for
228
+ * full DHT server mode (requires public IP).
229
+ */
230
+ export async function createHeliaOrbitDB(suffix = "", options = {}) {
231
+ const {
232
+ dhtClientMode = true,
233
+ useBootstrap = true,
234
+ useDHT = true,
235
+ maxConnections = 30,
236
+ } = options; // Default to client mode for better NAT compatibility
237
+
238
+ const listenAddresses = process.env.LIBP2P_LISTEN_ADDRS
239
+ ? process.env.LIBP2P_LISTEN_ADDRS.split(",").map((addr) => addr.trim())
240
+ : process.env.JEST_WORKER_ID || process.env.NODE_ENV === "test"
241
+ ? ["/ip4/127.0.0.1/tcp/0"]
242
+ : ["/ip4/0.0.0.0/tcp/0"];
243
+
244
+ const services = {
245
+ identify: identify(),
246
+ pubsub: gossipsub({ allowPublishToZeroTopicPeers: true }),
247
+ ping: ping(),
248
+ };
249
+ if (useDHT) {
250
+ services.dht = kadDHT({
251
+ clientMode: dhtClientMode,
252
+ });
253
+ }
254
+ // Bootstrapping is peer discovery, not a service.
255
+ const peerDiscovery = useBootstrap
256
+ ? [
257
+ bootstrap({
258
+ list: BOOTSTRAP_NODES,
259
+ tagName: "bootstrap",
260
+ tagValue: 50,
261
+ tagTTL: 120000, // 2 minutes
262
+ }),
263
+ ]
264
+ : [];
265
+
266
+ // Helia 7 builds the libp2p node itself, from these options — it does not
267
+ // take a node built elsewhere. Every key left out is filled from Helia's own
268
+ // defaults, which dial the public network (mDNS, public bootstrap peers,
269
+ // UPnP, a relay server), so each key this node depends on is spelled out,
270
+ // peerDiscovery included, even when it is empty.
271
+ const libp2pOptions = {
272
+ addresses: {
273
+ listen: listenAddresses,
274
+ },
275
+ transports: [
276
+ tcp({
277
+ // Enable connection limits - reduced to prevent excessive connections
278
+ maxConnections: {
279
+ inbound: 20,
280
+ outbound: 20,
281
+ },
282
+ }),
283
+ ],
284
+ connectionEncrypters: [noise()],
285
+ streamMuxers: [yamux()],
286
+ connectionManager: {
287
+ // Limit total connections to prevent excessive network usage
288
+ maxConnections,
289
+ },
290
+ peerDiscovery,
291
+ services,
292
+ };
293
+
294
+ const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
295
+ const blockstore = new LevelBlockstore(
296
+ `./orbitdb-bridge-${uniqueId}${suffix}`,
297
+ );
298
+ const datastore = new LevelDatastore(
299
+ `./orbitdb-bridge-${uniqueId}${suffix}-data`,
300
+ );
301
+
302
+ const helia = await createHelia({
303
+ libp2p: libp2pOptions,
304
+ blockstore,
305
+ datastore,
306
+ }).start();
307
+ const libp2p = helia.libp2p;
308
+ attachHeliaLogging(libp2p, suffix || "orbitdb");
309
+ const fs = unixfs(helia);
310
+ const orbitdb = await createOrbitDB({
311
+ ipfs: helia,
312
+ directory: `./orbitdb-bridge-${uniqueId}${suffix}-orbitdb`,
313
+ });
314
+
315
+ return { helia, orbitdb, libp2p, blockstore, datastore, unixfs: fs };
316
+ }
package/package.json ADDED
@@ -0,0 +1,172 @@
1
+ {
2
+ "name": "@le-space/orbitdb-storage-bridge",
3
+ "version": "0.10.0",
4
+ "description": "Back up, restore and replicate OrbitDB databases through pluggable storage backends, with hash and identity preservation",
5
+ "main": "lib/orbitdb-storacha-bridge.js",
6
+ "svelte": "dist/components/",
7
+ "type": "module",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/orbitdb-storacha-bridge.d.ts",
11
+ "default": "./lib/orbitdb-storacha-bridge.js"
12
+ },
13
+ "./backup-car": "./lib/backup-car.js",
14
+ "./courier-sync": "./lib/courier-sync.js",
15
+ "./pointer-ipns": "./lib/pointer-ipns.js",
16
+ "./dehydrate": "./lib/dehydrate.js",
17
+ "./restore-cid": "./lib/restore-cid.js",
18
+ "./gateway-fetch": "./lib/gateway-fetch.js",
19
+ "./backends/types": "./lib/backends/types.js",
20
+ "./backends/memory": "./lib/backends/memory.js",
21
+ "./backends/storacha": "./lib/backends/storacha.js",
22
+ "./backends/aleph": "./lib/backends/aleph.js",
23
+ "./backends/aleph-pin": "./lib/backends/aleph-pin.js",
24
+ "./backends/pinata": "./lib/backends/pinata.js",
25
+ "./backends/lighthouse": "./lib/backends/lighthouse.js",
26
+ "./memory-courier": "./lib/memory-courier.js",
27
+ "./StorachaIntegration.svelte": "./dist/components/StorachaIntegration.svelte",
28
+ "./StorachaAuth.svelte": "./dist/components/StorachaAuth.svelte",
29
+ "./storacha-backup": "./dist/components/storacha-backup.js",
30
+ "./components/*": "./dist/components/*"
31
+ },
32
+ "scripts": {
33
+ "test": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest",
34
+ "test:verbose": "npm test -- --verbose",
35
+ "test:integration": "npm test -- test/integration.test.js",
36
+ "test:p256-ucan": "npm test -- test/p256-ucan-security.test.js",
37
+ "test:car-backup": "npm test -- test/backup-car.test.js",
38
+ "test:timestamped-backup": "npm test -- test/timestamped-backup.test.js",
39
+ "test:network-download": "npm test -- test/network-download.test.js",
40
+ "test:backends": "npm test -- test/backends/",
41
+ "clear-space": "node examples/storacha/clear-space.js",
42
+ "car-demo": "node examples/storacha/car-backup-demo.js",
43
+ "ucan-demo": "node examples/storacha/ucan-demo.js",
44
+ "lint": "eslint --config eslint.config.js '{lib,examples,test}/**/*.js' --ignore-pattern 'examples/svelte/*/build/**' --ignore-pattern 'examples/svelte/*/dist/**' --ignore-pattern 'examples/svelte/**/.svelte-kit/**' --ignore-pattern 'node_modules/**'",
45
+ "format": "prettier --write lib/ examples/ test/ --ignore-path '.prettierignore'",
46
+ "clean": "rm -rf ./cid-bridge-test* ./demo-test*",
47
+ "build:components": "mkdir -p dist/components && cp src/components/*.svelte dist/components/ && cp src/components/*.js dist/components/",
48
+ "prepublishOnly": "npm run build:components"
49
+ },
50
+ "keywords": [
51
+ "orbitdb",
52
+ "storacha",
53
+ "filecoin",
54
+ "ipfs",
55
+ "backup",
56
+ "replication",
57
+ "web3",
58
+ "decentralized",
59
+ "database",
60
+ "cid",
61
+ "bridge",
62
+ "storage",
63
+ "restore",
64
+ "car",
65
+ "aleph",
66
+ "pinata",
67
+ "local-first"
68
+ ],
69
+ "files": [
70
+ "lib",
71
+ "dist",
72
+ "!dist/components/StorachaTest.svelte",
73
+ "!dist/components/StorachaTestWithReplication.svelte",
74
+ "!dist/components/StorachaTestWithWebAuthn.svelte",
75
+ "README.md",
76
+ "LICENSE"
77
+ ],
78
+ "author": "OrbitDB Storage Bridge Contributors",
79
+ "license": "MIT",
80
+ "publishConfig": {
81
+ "access": "public"
82
+ },
83
+ "dependencies": {
84
+ "@chainsafe/libp2p-noise": "^17.0.0",
85
+ "@chainsafe/libp2p-yamux": "^8.0.1",
86
+ "@helia/block-brokers": "^5.2.4",
87
+ "@helia/routers": "^5.1.1",
88
+ "@helia/unixfs": "^8.0.7",
89
+ "@ipld/car": "^5.4.1",
90
+ "@ipld/dag-cbor": "^9.0.6",
91
+ "@ipld/dag-ucan": "^3.4.5",
92
+ "@libp2p/bootstrap": "^12.0.27",
93
+ "@libp2p/crypto": "^5.1.21",
94
+ "@libp2p/gossipsub": "^16.1.0",
95
+ "@libp2p/identify": "^4.1.10",
96
+ "@libp2p/kad-dht": "^16.4.5",
97
+ "@libp2p/logger": "^6.2.13",
98
+ "@libp2p/ping": "^3.1.9",
99
+ "@libp2p/tcp": "^11.0.28",
100
+ "@orbitdb/core": "^4.0.0",
101
+ "@scure/bip39": "^1.3.0",
102
+ "@storacha/capabilities": "^2.0.0",
103
+ "@storacha/client": "^2.0.0",
104
+ "blockstore-level": "^4.0.1",
105
+ "cbor-web": "^10.0.11",
106
+ "datastore-level": "^13.0.1",
107
+ "dotenv": "^17.2.1",
108
+ "helia": "^7.1.12",
109
+ "libp2p": "^3.3.6",
110
+ "multiformats": "^14.0.5"
111
+ },
112
+ "devDependencies": {
113
+ "@eslint/js": "^10.0.1",
114
+ "@ibm/plex": "^6.4.1",
115
+ "@jest/globals": "^30.5.1",
116
+ "@le-space/ucanto-principal": "^9.1.1",
117
+ "@libp2p/websockets": "^10.1.17",
118
+ "@orbitdb/identity-provider-did": "^1.0.2",
119
+ "@storacha/did-mailto": "^1.0.1",
120
+ "@storacha/upload-api": "^3.0.0",
121
+ "@types/jest": "^30.0.0",
122
+ "@ucanto/client": "^9.0.2",
123
+ "@ucanto/core": "^10.4.0",
124
+ "@ucanto/principal": "^9.0.2",
125
+ "@ucanto/server": "^11.0.3",
126
+ "@ucanto/transport": "^9.2.1",
127
+ "@web3-storage/sigv4": "^1.0.2",
128
+ "carbon-components-svelte": "^0.89.7",
129
+ "carbon-icons-svelte": "^12.17.0",
130
+ "eslint": "^10.10.0",
131
+ "jest": "^30.5.1",
132
+ "key-did-provider-ed25519": "^4.0.2",
133
+ "key-did-resolver": "^4.0.0",
134
+ "prettier": "^3.0.3",
135
+ "prettier-plugin-svelte": "^3.4.0",
136
+ "prettier-plugin-tailwindcss": "^0.7.0"
137
+ },
138
+ "peerDependencies": {
139
+ "lucide-svelte": ">=0.400.0 <1.0.0",
140
+ "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0"
141
+ },
142
+ "engines": {
143
+ "node": ">=22.0.0"
144
+ },
145
+ "repository": {
146
+ "type": "git",
147
+ "url": "https://github.com/NiKrause/orbitdb-storage-bridge.git"
148
+ },
149
+ "bugs": {
150
+ "url": "https://github.com/NiKrause/orbitdb-storage-bridge/issues"
151
+ },
152
+ "homepage": "https://github.com/NiKrause/orbitdb-storage-bridge#readme",
153
+ "jest": {
154
+ "testEnvironment": "node",
155
+ "testTimeout": 60000,
156
+ "verbose": true,
157
+ "passWithNoTests": true,
158
+ "maxWorkers": 1,
159
+ "testPathIgnorePatterns": [
160
+ "/node_modules/",
161
+ "/examples/",
162
+ "/dist/",
163
+ "/coverage/"
164
+ ],
165
+ "moduleNameMapper": {
166
+ "^@peculiar/webcrypto$": "<rootDir>/node_modules/@peculiar/webcrypto/build/webcrypto.js"
167
+ }
168
+ },
169
+ "overrides": {
170
+ "test-exclude": "^8.0.0"
171
+ }
172
+ }