@le-space/orbitdb-storage-bridge 0.14.1 → 0.16.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/README.md CHANGED
@@ -1,348 +1,109 @@
1
1
  # OrbitDB Storage Bridge
2
2
 
3
- > **OrbitDB database backup, restoration and replication through pluggable storage backends, with hash and identity preservation**
4
-
3
+ > Back up, restore and replicate OrbitDB databases through pluggable storage backends, with hash and identity preservation.
5
4
 
6
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
6
  [![Node.js](https://img.shields.io/badge/Node.js-22+-green.svg)](https://nodejs.org/)
8
7
  [![CI/CD Pipeline](https://github.com/NiKrause/orbitdb-storage-bridge/actions/workflows/ci.yml/badge.svg)](https://github.com/NiKrause/orbitdb-storage-bridge/actions/workflows/ci.yml)
9
- [![ESLint](https://img.shields.io/badge/ESLint-passing-brightgreen.svg)](https://github.com/NiKrause/orbitdb-storage-bridge/actions/workflows/ci.yml)
10
8
  [![npm version](https://img.shields.io/npm/v/@le-space/orbitdb-storage-bridge.svg)](https://www.npmjs.com/package/@le-space/orbitdb-storage-bridge)
11
9
 
12
10
  > [!NOTE]
13
- > **Renamed from `orbitdb-storacha-bridge`.** Up to 0.6.0 this package was published as
14
- > `orbitdb-storacha-bridge`. From 0.7.0 it is `@le-space/orbitdb-storage-bridge`, because Storacha is no
15
- > longer the only backend it bridges to. The rename changes no API — swap the dependency and the
16
- > import specifiers:
17
- >
18
- > ```bash
19
- > npm uninstall orbitdb-storacha-bridge
20
- > npm install @le-space/orbitdb-storage-bridge
21
- > ```
22
- >
23
- > `orbitdb-storacha-bridge/courier-sync` becomes `@le-space/orbitdb-storage-bridge/courier-sync`, and so on
24
- > for every entry point. Names that refer to Storacha itself stay as they are:
25
- > `OrbitDBStorachaBridge`, `StorachaIntegration.svelte`, `backends/storacha`, the `storacha_*`
26
- > localStorage keys that hold saved logins — and so does the debug namespace
27
- > `libp2p:orbitdb-storacha:*`.
28
-
29
-
30
- > [!IMPORTANT]
31
- > **The Storacha upload service is gone.** Writes were switched off in May 2026 and the
32
- > service has since been decommissioned. Backup no longer works, and restore only reaches
33
- > blocks that something other than Storacha still holds. See
34
- > [Status: Storacha sunset](#status-storacha-sunset-may-2026) and
35
- > [docs/STORAGE-BACKENDS.md](docs/STORAGE-BACKENDS.md) for where to go instead.
36
-
37
-
38
- ## Table of Contents
39
-
40
- - [OrbitDB Storage Bridge](#@le-space/orbitdb-storage-bridge)
41
- - [Table of Contents](#table-of-contents)
42
- - [Status: Storacha sunset (May 2026)](#status-storacha-sunset-may-2026)
43
- - [What we want to accomplish](#what-we-want-to-accomplish)
44
- - [The Challenge of Distributed Data Persistence](#the-challenge-of-distributed-data-persistence)
45
- - [Architectural Considerations for Local-First Applications](#architectural-considerations-for-local-first-applications)
46
- - [Use Cases](#use-cases)
47
- - [Architecture Notes](#architecture-notes)
48
- - [What This Does](#what-this-does)
49
- - [Roadmap](#roadmap)
50
- - [Installation](#installation)
51
- - [Environment Setup](#environment-setup)
52
- - [Demo](#demo)
53
- - [NodeJS Demo Scripts (full backup with Manifest, Identity and AccessController and entries blocks)](#nodejs-demo-scripts-full-backup-with-manifest-identity-and-accesscontroller-and-entries-blocks)
54
- - [Svelte Components](#svelte-components)
55
- - [How It Works](#how-it-works)
56
- - [Restore Mechanism](#restore-mechanism)
57
- - [Logging](#logging)
58
- - [Testing](#testing)
59
- - [Contributing](#contributing)
60
- - [License](#license)
61
-
62
-
63
- ## Does this work from a browser? Measure it
64
-
65
- `examples/browser/storage-probe/` is a static page that asks the services this package talks to,
66
- from a real browser, with no server: do Aleph, Pinata and Lighthouse answer a page at all, does
67
- their CORS survive a refusal as well as a success, and does anybody on IPFS hold a given CID at an
68
- address a browser can dial. With your own key it does a real upload and reads it back. The key is
69
- kept in that browser and sent only to the service it belongs to.
70
-
71
- It is published from this repository's Pages, and it opens from a checkout just as well — there is
72
- no build step, which is the point: what it measures is a browser talking to a service, with
73
- nothing in between.
74
-
75
- ## Status: Storacha sunset (May 2026)
76
-
77
- Storacha switched off user writes in **May 2026** and has since decommissioned the service.
78
- Verified on 2026-09-05:
79
-
80
- | Check | Result |
81
- | --- | --- |
82
- | `up.storacha.network`, `console.storacha.network`, `indexer.storacha.network`, `forge.storacha.network` | no DNS record — upload, console and indexing endpoints are gone |
83
- | `storacha.network`, `docs.storacha.network` | `301` → `fil.one`, the team's new S3-compatible product |
84
- | `storacha.link`, `w3s.link` | `301` → `dweb.link`; the gateways only forward to the public IPFS gateway now |
85
- | the widget demo CID linked in the roadmap below | `504` on `w3s.link`, `dweb.link`, `ipfs.io` and `trustless-gateway.link` |
86
- | `@storacha/client` on npm | last release `2.1.4`, 2026-05-15, not marked deprecated |
87
-
88
- The gateway those redirects pointed at is gone too. On **2026-09-21** Protocol Labs retired
89
- `ipfs.io` and `dweb.link`: both answer `429` with an RFC 8594 `Sunset` header and a link to
90
- [gatewaychanges.ipfs.io](https://gatewaychanges.ipfs.io/), so `storacha.link` and `w3s.link`
91
- now redirect to a closed door. Retrieval defaults here are `ipfs.aleph.cloud` — the one free
92
- path gateway measured still serving arbitrary CIDs on 2026-09-23 — with
93
- `trustless-gateway.link` available for verifiable single-block requests
94
- (`Accept: application/vnd.ipld.raw`). One host is not a fallback chain, which is why `peer-fetch.js` fetches from the providers that
95
- hold the blocks instead — bitswap over libp2p, measured from a real page at 0.73 s to dial and
96
- 0.26 s for the block, with no credential in the path. See
97
- [docs/RECOVERY-ON-A-SECOND-DEVICE.md](docs/RECOVERY-ON-A-SECOND-DEVICE.md).
98
-
99
- The shutdown is traceable in the open:
100
- [`upload-service#708`](https://github.com/storacha/upload-service/pull/708) added a `writesDisabled`
101
- kill switch that makes the eight user-initiated write capabilities
102
- (`space/blob/{add,remove,replicate}`, `space/index/add`, `upload/{add,remove}`, `store/{add,remove}`)
103
- return `ServiceUnavailable`, and [`w3infra#636`](https://github.com/storacha/w3infra/pull/636) wired
104
- `WRITES_DISABLED=true` into the production stack — both merged 2026-05-15. Five days later Storacha
105
- shipped `storacha space migrate`
106
- ([`@storacha/filecoin-pin-migration`](https://www.npmjs.com/package/@storacha/filecoin-pin-migration)),
107
- a migration path from Storacha spaces to Filecoin Onchain Cloud. That tool reads spaces through
108
- endpoints that no longer resolve, so the official migration window has closed.
109
-
110
- We found no announcement page: the Storacha blog now redirects to `fil.one/blog`, which carries a
111
- single post ("Introducing Fil One", 2026-08-12). The dates above come from the code and the DNS,
112
- not from a press release.
113
-
114
- **What this means for this library**
115
-
116
- - **Backup does not work.** Every write path ends in `client.uploadFile()` against `up.storacha.network`.
117
- - **Restore only reaches what someone else still holds.** The p2p-first path still finds blocks that a
118
- peer or another pinning service pins; the gateway fallback and the `capability.upload.list`
119
- discovery step cannot reach Storacha any more.
120
- - **The OrbitDB half is unaffected.** Block extraction, CID bridging, CAR packing, identity
121
- preservation, UCAN signing and courier-sync are backend-agnostic — only the handful of Storacha
122
- client methods mapped in [docs/STORAGE-BACKENDS.md](docs/STORAGE-BACKENDS.md) need a new home.
123
-
124
- If you still have an OrbitDB instance with the blocks in it, re-pin them somewhere else now: the data
125
- is only as alive as the peers that hold it.
126
-
127
-
128
- ## What we want to accomplish
129
-
130
- ### The Challenge of Distributed Data Persistence
131
-
132
- In local-first, peer-to-peer applications built on OrbitDB, data naturally replicates across participating peers through libp2p network connections. Under ideal conditions, this distributed architecture provides inherent redundancy—if one peer loses data, they can resynchronize from other active peers in the network. This peer-to-peer replication model represents the current state of OrbitDB technology.
133
-
134
- While relay nodes and pinning services (running Helia and OrbitDB instances) can provide additional decentralized persistence for database entries and IPFS-referenced content, the ecosystem still lacks comprehensive infrastructure for long-term archival of large-scale OrbitDB deployments.
135
-
136
- ### Architectural Considerations for Local-First Applications
137
-
138
- OrbitDB's data model differs fundamentally from traditional centralized databases. In local-first architectures, users typically host their own data locally and selectively replicate with specific peers based on collaboration requirements—not with the entire network. This selective replication is essential for scalability and user experience.
139
-
140
- However, as OrbitDB instances grow (consider a blog database accumulating years of posts), replication times increase proportionally. At scale, databases require archival strategies and potential sharding to maintain performant synchronization and optimal user experience.
141
-
142
- ### Use Cases
143
-
144
- This bridge addresses several critical scenarios:
145
-
146
- **1. Long-term Archival**
147
- Archive large OrbitDB instances to Storacha/Filecoin storage, enabling efficient cold storage for historical data while maintaining fast replication of active datasets.
148
-
149
- **2. Disaster Recovery**
150
- Provides recovery options when all active peers lose data simultaneously. Users can restore databases from Storacha using either their original identity or a new identity, ensuring business continuity beyond the peer-to-peer network's availability.
151
-
152
- **3. Network Resilience**
153
- Historically, various network environments (corporate networks, ISPs, regional restrictions) have blocked critical protocols including WebRTC and WebSocket/WebTransport. While libp2p's multi-transport architecture provides numerous fallback options, having an additional restoration pathway through IPFS/Storacha offers defense-in-depth for network-hostile environments. Users can restore databases directly from IPFS and maintain incremental backups after each database mutation.
154
-
155
- **4. Access Control & Delegation**
156
- The bridge supports UCAN (User Controlled Authorization Networks) authentication with planned delegation capabilities between OrbitDB instances. This enables fine-grained, time-bound access control for Storacha backup spaces, allowing users to securely share access with collaborators or recovery agents.
157
-
158
- ### Architecture Notes
159
-
160
- Currently, Storacha backup and restore operations utilize Storacha's gateway infrastructure to interface with Filecoin's decentralized storage network. This hybrid approach balances accessibility with decentralization during the current phase of the Filecoin ecosystem's evolution.
161
-
162
- That description is now historical: the gateway infrastructure it relies on was decommissioned in 2026 (see [Status: Storacha sunset](#status-storacha-sunset-may-2026)). The hybrid shape of the design still holds — a backend that stores bytes, an IPFS network that serves them — but the backend slot is open. [docs/STORAGE-BACKENDS.md](docs/STORAGE-BACKENDS.md) compares the candidates.
163
-
164
- ## What This Does
165
-
166
- Backup and restore between **OrbitDB databases** and **Storacha/Filecoin** with full hash and identity preservation. Works in both Node.js and browser environments.
167
-
168
- The project includes **Svelte components** for browser-based demos and integration (see [SVELTE-COMPONENTS.md](SVELTE-COMPONENTS.md) for detailed documentation).
169
-
170
- **Features:**
171
-
172
- - backup/restore between OrbitDB and Storacha in browsers and NodeJS via Storacha key and proof credential
173
- - full backup per space
174
- - timestamped backups (multiple backups per space - restore last backup by default)
175
- - Storacha Svelte components for integration into Svelte projects
176
- - UCAN authentication
177
- - Backup/restore functionality with hash and identity preservation
178
- - OrbitDB CAR file storage [OrbitDB CustomStorage](https://github.com/orbitdb/orbitdb/blob/main/docs/STORAGE.md)
179
-
180
- ## Roadmap
181
-
182
- > Being re-based on a backend interface instead of a single vendor — the plan is
183
- > [issue 54](https://github.com/NiKrause/orbitdb-storage-bridge/issues/54), not here. The WebAuthn/varsig items below survive
184
- > unchanged; the Storacha-named ones become backend-agnostic.
185
-
186
- - [ ] Live parallel persistence: hand an open database a backend-backed OrbitDB `ComposedStorage`, so every block is written to a backend **as it is created** — during sync and after each update — rather than only when a backup runs.
187
- - [ ] Today a block becomes durable somewhere else only if bitswap reaches a peer that keeps it, gossipsub reaches a subscriber, or somebody runs a backup. All three depend on another party being reachable at that moment; a write-through storage does not.
188
- - [ ] The seam already exists: [`lib/car-storage.js`](lib/car-storage.js) implements OrbitDB's storage shape (`put`/`get`/`del`/`iterator`/`merge`) against a CAR file, and `ComposedStorage` is designed to fan a write out to more than one place. This points the same seam at a network backend instead.
189
- - [ ] Aleph Cloud is the first candidate, because it is the only evaluated backend a browser can write to with no key at all — `POST /api/v0/add` answers unauthenticated with an open CORS header. See [docs/STORAGE-BACKENDS.md](docs/STORAGE-BACKENDS.md); persistence there needs the wallet-signed STORE message, not the ingest alone.
190
- - [ ] Open question worth settling first: what a write-through storage does when the backend is unreachable. Blocking a local write on a network round trip would make the database only as available as the backend, which is the opposite of the point — so it queues, and the queue is the design.
191
-
192
- - [ ] v0.4.4 (Feb 2026): Latest-backup pointer (single CID) to avoid listing via the Storacha SDK and restore from the IPFS network for initial OrbitDB syncs.
193
- - [ ] After each backup, write a small pointer record (JSON) that stores the latest metadata CID, CAR CID, and last heads (block CID).
194
- - [ ] Store that pointer in a user-controlled place (local storage, QR/share link, WebAuthN largetBlog extension or file download).
195
- - [ ] v0.5.0 (Feb 2026): OrbitDB CustomStorage (StorachaStorage) ([issue 23](https://github.com/NiKrause/orbitdb-storage-bridge/issues/23)).
196
- - [ ] v0.6.0 (Mar 2026): WebAuthN + varsig signing/verification (Ed25519 and P-256) for OrbitDB oplog. https://github.com/ChainAgnostic/varsig/blob/main/README.md
197
- - [ ] v0.6.1 (Mar 2026): WebAuthN + SimpleEncryption example that uses WebAuthN+PRF key material for encrypted backups and restore.
198
- - [ ] v0.7.0 (Apr 2026): WebAuthN + OrbitDB AccessController (store a UCAN instead of only a DID for admin/write access).
199
- - [ ] Alice (authenticated via UCAN or Storacha credentials) can delegate/revoke access for Bob with custom/default capabilities ([issue 16](https://github.com/NiKrause/orbitdb-storage-bridge/issues/16)). See [WebAuthN Upload Wall](https://github.com/NiKrause/ucan-upload-wall/tree/browser-only/web) and the [live demo](https://bafybeibdcnp7pr26okzr6kbygcounsz3klyg3vydxwwovmz2ljyzfmprre.ipfs.w3s.link/).
200
- - [ ] v0.7.1 (May 2026): Storacha Backup & Restore Svelte widget with WebAuthN-varsig UCAN signing/verification (Ed25519/P-256).
201
- - [ ] v0.7.2 (May 2026): Storacha Backup & Restore React widget with WebAuthN-varsig UCAN signing/verification (Ed25519/P-256).
202
- - [ ] v0.7.3 (May 2026): Storacha Backup & Restore React widget with WebAuthN-varsig UCAN delegation (Ed25519/P-256).
203
- - [ ] v0.7.4 (May 2026): UI enhancement for the Storacha Backup & Restore widget (timestamped backup restore and management).
204
- - [ ] v0.8.0 (Jun 2026): Upgrade to UCAN 1.0 support.
205
- - [ ] v0.9.0 (Jul 2026): Social backup between devices with DKG (decentralized key generation).
206
- - [ ] v0.10.0 (Aug 2027): WebAuthN + Roaming Credentials: Have a browser and a mobile with one Yubikey creating one and the same DID and replicating the same OrbitDB
207
-
208
- Read more on Medium: [Bridging OrbitDB with Storacha: Decentralized Database Backups](https://medium.com/@akashjana663/bridging-orbitdb-with-storacha-decentralized-database-backups-44c7bee5c395)
209
-
210
- ## Installation
211
-
212
- Install the package via npm:
11
+ > Published as `orbitdb-storacha-bridge` up to 0.6.0 and as `@le-space/orbitdb-storage-bridge`
12
+ > from 0.7.0, because Storacha is no longer the only backend it bridges to. The rename changed no
13
+ > API — only the dependency and the import specifiers. Names that refer to Storacha itself stay as
14
+ > they are: `OrbitDBStorachaBridge`, `backends/storacha`, and the `libp2p:orbitdb-storacha:*` debug
15
+ > namespace.
16
+
17
+ ## What it does
18
+
19
+ An OrbitDB database is a set of content-addressed blocks and a log whose heads tie them together.
20
+ This package moves those blocks somewhere else and brings them back with their CIDs and the
21
+ writer's identity intact — so a restored database is the same database, and its original author can
22
+ still write to it.
23
+
24
+ Where "somewhere else" is, is a choice:
25
+
26
+ | Backend | Needs | |
27
+ | --- | --- | --- |
28
+ | `aleph` | nothing | the only backend a browser can write to with no key; ingest only |
29
+ | `aleph-pin` | a wallet | the signed STORE message that makes Aleph keep it |
30
+ | `pinata` | a scoped JWT | pin-by-CID as well as CAR upload |
31
+ | `lighthouse` | an API key | pay once, stored in perpetuity |
32
+ | `memory` | nothing | in-process, for tests and demos |
33
+ | `mirror` | — | wraps others: one backup, several services |
34
+ | `encryption` | — | wraps one: the service holds ciphertext |
35
+
36
+ Reading back needs no account, because a CID is a name anyone can resolve: `peer-fetch` asks the
37
+ peers that hold the blocks over libp2p, `gateway-fetch` asks an HTTP gateway. Two more entry
38
+ points do not involve a storage service at all — `courier-sync` replicates over any byte courier
39
+ (a LoRa mesh, a QR relay, a file), and `dehydrate`/`hydrate` put a database where a second device
40
+ can find it from a passkey alone.
41
+
42
+ ## Install
213
43
 
214
44
  ```bash
215
45
  npm install @le-space/orbitdb-storage-bridge
216
46
  ```
217
47
 
218
- ## Environment Setup
219
-
220
- `STORACHA_KEY` and `STORACHA_PROOF` in `.env` are still what the code reads, and existing credentials still parse — but there is no longer a service to present them to, and no way to mint new ones: the console and the quickstart docs are gone (see [Status: Storacha sunset](#status-storacha-sunset-may-2026)). The test suite's in-memory modes run without credentials; see `test/README.md`.
221
-
222
- ## Demo
223
-
224
- ### NodeJS Demo Scripts (full backup with Manifest, Identity and AccessController and entries blocks)
48
+ ## Quick start
225
49
 
226
- They back up to Aleph by default, which takes an upload without an account, so
227
- they run straight after cloning. `STORAGE=pinata` and `STORAGE=lighthouse`
228
- switch to the paid services and read their keys from the environment — see
229
- [`examples/storage.js`](examples/storage.js).
50
+ The demos back up to Aleph, which accepts an upload without an account, so they run straight after
51
+ cloning:
230
52
 
231
53
  ```sh
232
- node examples/backup-demo.js # prints the backup's CID
233
- BACKUP_CID=<cid> node examples/restore-demo.js # restores it on a fresh node
54
+ node examples/backup-demo.js # prints the backup's CID
55
+ BACKUP_CID=<cid> node examples/restore-demo.js # restores it on a node that has never seen it
234
56
  ```
235
57
 
236
- - `node` [`examples/demo.js`](examples/demo.js) - Complete backup/restore cycle, including what stops the second node from writing
237
- - `node` [`examples/backup-demo.js`](examples/backup-demo.js) - Backup only; prints the CID the restore needs
238
- - `node` [`examples/restore-demo.js`](examples/restore-demo.js) - Restore from that CID onto a node that has never seen the database
239
- - `node` [`examples/demo-different-identity.js`](examples/demo-different-identity.js) - Different identities with access control enforcement
240
- - `node` [`examples/demo-shared-identities.js`](examples/demo-shared-identities.js) - Shared identity backup/restore scenarios
241
-
242
- An Aleph upload is not kept: staying stored takes a wallet-signed STORE message
243
- this library does not send, so restore what you back up while the demo is still
244
- running.
245
-
246
- The scripts written against Storacha's space and UCAN model are in
247
- [`examples/storacha/`](examples/storacha/README.md). None of them runs end to
248
- end since the uploads stopped, and the README there says what replaced each.
249
-
250
- ### In a browser: a database back on a device that has nothing
251
-
252
- [**funkpost's recovery page**](https://nikrause.github.io/funkpost/recovery/) runs the whole
253
- [recovery procedure](docs/RECOVERY-ON-A-SECOND-DEVICE.md) in a phone's browser, with a
254
- security key and nothing else ([source](https://github.com/NiKrause/funkpost/tree/main/examples/recovery)):
58
+ `STORAGE=pinata` and `STORAGE=lighthouse` switch backends and read their keys from the environment
59
+ — see [`examples/storage.js`](examples/storage.js). An Aleph upload is not retained without the
60
+ signed STORE message, so restore what you back up while the demo is still running.
255
61
 
256
- 1. **the key gives the identity** — the DID, and a signing key derived from its PRF output,
257
- the same on every device;
258
- 2. **a list** is made and written to;
259
- 3. **`dehydrate`** backs it up to Aleph as a CAR, and publishes an IPNS pointer under a name
260
- the key derives;
261
- 4. **on another device** — or the same one, wiped — the same key finds the pointer, **`hydrate`**
262
- brings the list back, and the list takes new entries, because the writer is the same.
62
+ Further scripts: identity and access-control scenarios in [`examples/`](examples/), and the
63
+ Storacha-era scripts in [`examples/storacha/`](examples/storacha/README.md), none of which runs end
64
+ to end any more.
263
65
 
264
- On 21 September 2026 it ran that way on two phones: a Galaxy Fold 5 backed up and was reset,
265
- and a Galaxy A57 with the same key brought the list back and wrote to it. The page says at
266
- every step which service it contacts; the technical details sit behind one button.
66
+ ## Recovery on a second device
267
67
 
268
- ### Svelte Components
68
+ [The recovery page](https://nikrause.github.io/orbitdb-storage-bridge/recovery/) runs the whole
69
+ procedure in a phone's browser with a security key and nothing else: the passkey gives the
70
+ identity, `dehydrate` backs the database up and publishes an IPNS pointer under a name derived from
71
+ the key, and on another device the same key finds that pointer — so `hydrate` needs no CID, no
72
+ address and no file. The restored database takes new entries, because the writer is the same.
269
73
 
270
- For browser-based integration, this project includes Svelte components for authentication, backup/restore, P2P replication, and WebAuthn biometric authentication. See [**SVELTE-COMPONENTS.md**](SVELTE-COMPONENTS.md) for complete documentation of all available components and demonstrations.
74
+ On 21 September 2026 it ran that way on two phones: a Galaxy Fold 5 backed up and was reset, and a
75
+ Galaxy A57 with the same key brought the database back and wrote to it. Source:
76
+ [`examples/svelte/recovery/`](examples/svelte/recovery).
271
77
 
272
- That browser side is older than this repository's copy of it, and the UCAN delegation work now sits on a branch. [docs/STORACHA-UI-HISTORY.md](docs/STORACHA-UI-HISTORY.md) records what existed, what it could do, and where each piece is today.
78
+ ## Documentation
273
79
 
274
- ## How It Works
275
-
276
- 1. **Extract Blocks** - Separates OrbitDB database into individual components (log entries, manifest, identities, access controls)
277
- 2. **Upload to Storacha** - Each block is uploaded separately to IPFS/Filecoin via Storacha
278
- 3. **Block Discovery** - Lists all files in Storacha space using Storacha SDK APIs
279
- 4. **CID Bridging** - Converts between Storacha CIDs (`bafkre*`) and OrbitDB CIDs (`zdpu*`)
280
- 5. **Reconstruct Database** - Reassembles blocks and opens database with original identity
281
-
282
- ## Restore Mechanism
283
-
284
- The restore process uses a **ipfs-p2p-first approach with ipfs-http-gateway fallback** for downloading backups for restore. File listing and metadata discovery are currently performed via the Storacha SDK (using Storacha gateway API). We are working on an **IPNS-based mechanism** to find the latest heads blocks and OrbitDB address directly from the IPFS network via IPNS, eliminating the need to list all files via the centralized Storacha gateway API.
285
-
286
- ## Logging
287
-
288
- The library uses **@libp2p/logger** for consistent logging across the libp2p ecosystem. Control logging with the `DEBUG` environment variable:
289
-
290
- **Node.js:**
291
- ```bash
292
- # Enable all logs from this library (the namespace kept its old name)
293
- DEBUG=libp2p:orbitdb-storacha:* node your-script.js
294
-
295
- # Enable specific components
296
- DEBUG=libp2p:orbitdb-storacha:bridge node your-script.js
297
-
298
- # Enable all libp2p logs (includes this library + libp2p internals)
299
- DEBUG=libp2p:* node your-script.js
300
- ```
301
-
302
- **Browser:**
303
- ```javascript
304
- // In browser console or before loading the application
305
- localStorage.setItem('debug', 'libp2p:orbitdb-storacha:*')
306
- // Then refresh the page
307
- ```
308
-
309
- The logger supports printf-style formatting:
310
- - `%s` - string
311
- - `%d` - number
312
- - `%o` - object
313
- - `%p` - peer ID
314
- - `%b` - base58btc encoded data
315
- - `%t` - base32 encoded data
316
-
317
- ## Testing
318
-
319
- See `test/README.md` for detailed test documentation, modes (in-memory vs production),
320
- and how to run each suite.
80
+ | | |
81
+ | --- | --- |
82
+ | [docs/DESIGN.md](docs/DESIGN.md) | the problem this solves, and how a backup and a restore actually work |
83
+ | [docs/STORAGE-BACKENDS.md](docs/STORAGE-BACKENDS.md) | every backend evaluated — prices, limits, and what has been verified against a live account |
84
+ | [docs/RECOVERY-ON-A-SECOND-DEVICE.md](docs/RECOVERY-ON-A-SECOND-DEVICE.md) | the passkey recovery procedure, step by step |
85
+ | [docs/CAR-BACKUP.md](docs/CAR-BACKUP.md) | CAR-based timestamped backups |
86
+ | [SVELTE-COMPONENTS.md](SVELTE-COMPONENTS.md) | the browser components — older than this repository's copy of them; [docs/STORACHA-UI-HISTORY.md](docs/STORACHA-UI-HISTORY.md) records what each piece could do and where it went |
87
+ | [docs/LOGGING.md](docs/LOGGING.md) | debug namespaces, in Node and in a browser |
88
+ | [ROADMAP.md](ROADMAP.md) | what is planned |
89
+ | [docs/STORACHA-SUNSET.md](docs/STORACHA-SUNSET.md) | why Storacha stopped being the default, with dates and evidence |
90
+ | [test/README.md](test/README.md) | the suites, and which of them need credentials |
91
+
92
+ `examples/browser/storage-probe/` is a static page that asks these services from a real browser
93
+ with no build step: whether they answer a page at all, whether their CORS survives a refusal as
94
+ well as a success, and whether anyone on IPFS holds a given CID at an address a browser can dial.
321
95
 
322
96
  ## Contributing
323
97
 
324
- 1. Fork the repository
325
- 2. Create a feature branch
326
- 3. Add tests for new functionality
327
- 4. Submit a pull request
98
+ Fork, branch, add tests for new behaviour, open a pull request.
328
99
 
329
100
  ## License
330
101
 
331
- MIT License
332
-
333
- ## Trademarks
334
-
335
- LoRa® is a trademark of Semtech Corporation. Meshtastic® is a registered
336
- trademark of Meshtastic LLC. Other names used here — OrbitDB, IPFS, Filecoin,
337
- Storacha, Pinata, Lighthouse, Aleph — belong to their respective owners and
338
- appear only to say what this package interoperates with. This project is not
339
- affiliated with or endorsed by any of them.
102
+ MIT.
340
103
 
341
- The first two appear in this repository only in prose about what the courier
342
- seam is *for*: `courier-sync` is transport-neutral and this package contains no
343
- code from either project and depends on neither. That is deliberate rather than
344
- incidental. The Meshtastic client libraries (`@meshtastic/core`,
345
- `@meshtastic/transport-web-bluetooth`) are **GPL-3.0-only**, and the courier
346
- that drives them lives in [funkpost](https://github.com/NiKrause/funkpost), on
347
- the GPL side of that line. This package is MIT and stays that way; the door
348
- only opens one way.
104
+ LoRa® is a trademark of Semtech Corporation and Meshtastic® a registered trademark of Meshtastic
105
+ LLC; OrbitDB, IPFS, Filecoin, Storacha, Pinata, Lighthouse and Aleph belong to their respective
106
+ owners and appear here only to say what this package interoperates with. This project is
107
+ affiliated with none of them and contains no code from the Meshtastic libraries, which are
108
+ GPL-3.0-only — see [docs/TRADEMARKS.md](docs/TRADEMARKS.md) for why that line matters and which
109
+ side of it this package is on.
@@ -65,16 +65,21 @@ async function sha256Hex(payload) {
65
65
  * Exported so it can be tested without a wallet, and read without running one.
66
66
  *
67
67
  * @param {object} args
68
- * @param {string} args.sender - the wallet address
68
+ * @param {string} args.sender - the wallet address that signs
69
+ * @param {string} [args.owner] - the account the STORE is for, when the sender
70
+ * signs on its behalf (see {@link createAlephAuthorizer}); the sender itself
71
+ * by default
69
72
  * @param {string} args.cid - what to keep
70
73
  * @param {string} [args.channel]
71
74
  * @param {number} [args.now] - seconds; injected so a test is not a clock
72
75
  * @param {(payload: string) => Promise<string>} [args.hasher]
73
76
  */
74
- export async function buildStoreMessage({ sender, cid, channel = DEFAULT_ALEPH_CHANNEL, now, hasher = sha256Hex }) {
77
+ export async function buildStoreMessage({ sender, owner, cid, channel = DEFAULT_ALEPH_CHANNEL, now, hasher = sha256Hex }) {
75
78
  const time = now ?? Date.now() / 1000;
76
79
  const content = {
77
- address: sender,
80
+ // Whose STORE this is. Equal to the sender unless the owner's `security`
81
+ // aggregate authorizes the sender to send it on the owner's behalf.
82
+ address: owner || sender,
78
83
  // "the thing to keep is an IPFS CID" — not the envelope's item_type
79
84
  item_type: "ipfs",
80
85
  item_hash: cid,
@@ -102,7 +107,9 @@ export const signaturePayload = (message) =>
102
107
  * A `pin` function for {@link createAlephBackend}.
103
108
  *
104
109
  * @param {object} options
105
- * @param {string} options.sender - the wallet address doing the keeping
110
+ * @param {string} options.sender - the wallet address that signs
111
+ * @param {string} [options.owner] - the account the STORE is for, when it has
112
+ * authorized `sender` (a delegate) to send STORE messages on its behalf
106
113
  * @param {(address: string, message: string) => Promise<string>} options.sign -
107
114
  * `personal_sign`, or anything shaped like it. The library never sees a key.
108
115
  * @param {string} [options.apiHost]
@@ -113,7 +120,7 @@ export const signaturePayload = (message) =>
113
120
  * @returns {(cid: string, meta?: object) => Promise<{ itemHash: string, status: string }>}
114
121
  */
115
122
  export function createAlephPin(options = {}) {
116
- const { sender, sign } = options;
123
+ const { sender, sign, owner } = options;
117
124
  const apiHost = options.apiHost || DEFAULT_ALEPH_API_HOST;
118
125
  const channel = options.channel || DEFAULT_ALEPH_CHANNEL;
119
126
  const hasher = options.hasher || sha256Hex;
@@ -125,7 +132,7 @@ export function createAlephPin(options = {}) {
125
132
  if (typeof doFetch !== "function") throw new BackendError("INVALID_BACKEND", "createAlephPin needs fetch");
126
133
 
127
134
  return async function pin(cid) {
128
- const unsigned = await buildStoreMessage({ sender, cid, channel, hasher, now: now?.() });
135
+ const unsigned = await buildStoreMessage({ sender, owner, cid, channel, hasher, now: now?.() });
129
136
  const signature = await sign(sender, signaturePayload(unsigned));
130
137
  const message = {
131
138
  ...unsigned,
@@ -158,4 +165,138 @@ export function createAlephPin(options = {}) {
158
165
  };
159
166
  }
160
167
 
168
+ /** The reserved channel and aggregate key Aleph keeps permissions in. */
169
+ export const SECURITY = "security";
170
+
171
+ /**
172
+ * @typedef {object} AlephAuthorization one entry of an owner's `security.authorizations`
173
+ * @property {string} address - the delegate, which may then send on the owner's behalf
174
+ * @property {string[]} [types] - e.g. `["STORE"]`; every type when absent
175
+ * @property {string[]} [channels] - e.g. `["BELEGE-BACKUP"]`; every channel when absent
176
+ * @property {string} [chain] - only the delegate's address on this chain, e.g. `"ETH"`
177
+ * @property {string[]} [post_types]
178
+ * @property {string[]} [aggregate_keys]
179
+ */
180
+
181
+ /**
182
+ * Build the unsigned AGGREGATE that sets an owner's authorizations.
183
+ *
184
+ * Aleph keeps permissions in the owner's `security` aggregate, written only by
185
+ * the owner itself (`sender == content.address`) on the `security` channel. An
186
+ * aggregate key is replaced as a whole, so `authorizations` is the **complete**
187
+ * list after the change, not an addition: {@link createAlephAuthorizer} reads
188
+ * the current list first.
189
+ *
190
+ * @param {object} args
191
+ * @param {string} args.owner - the account granting, and signing
192
+ * @param {AlephAuthorization[]} args.authorizations
193
+ * @param {number} [args.now] - seconds
194
+ * @param {(payload: string) => Promise<string>} [args.hasher]
195
+ */
196
+ export async function buildAuthorizationMessage({ owner, authorizations, now, hasher = sha256Hex }) {
197
+ if (!owner) throw new BackendError("INVALID_BACKEND", "an authorization needs the owner's address");
198
+ if (!Array.isArray(authorizations) || authorizations.some((a) => typeof a?.address !== "string" || !a.address)) {
199
+ throw new BackendError("INVALID_BACKEND", "every authorization needs the delegate's address");
200
+ }
201
+ const time = now ?? Date.now() / 1000;
202
+ const item_content = JSON.stringify({
203
+ address: owner,
204
+ key: SECURITY,
205
+ content: { authorizations },
206
+ time,
207
+ });
208
+ return {
209
+ sender: owner,
210
+ chain: "ETH",
211
+ type: "AGGREGATE",
212
+ item_hash: await hasher(item_content),
213
+ item_type: "inline",
214
+ item_content,
215
+ time,
216
+ channel: SECURITY,
217
+ };
218
+ }
219
+
220
+ /**
221
+ * Let other keys send STORE messages for an account: read, grant and revoke its
222
+ * authorizations.
223
+ *
224
+ * The use it was built for: one funded account pays for keeping backups, and
225
+ * the keys that actually sign them are others — a key derived from a passkey in
226
+ * the browser, one per person or device. Each is granted only what it needs
227
+ * (`types: ["STORE"]`, one channel), and revoked on its own. The owner signs
228
+ * these grants; the delegates then pass `owner` to {@link createAlephPin}.
229
+ *
230
+ * Which balance Aleph charges for a STORE sent by a delegate is not stated in
231
+ * its documentation; measure it before relying on it.
232
+ *
233
+ * Aleph keys accounts by their EIP-55 checksummed address; pass `owner` in that
234
+ * form, or `read()` finds nothing.
235
+ *
236
+ * @param {object} options
237
+ * @param {string} options.owner - the account granting
238
+ * @param {(address: string, message: string) => Promise<string>} options.sign - the owner's `personal_sign`
239
+ * @param {string} [options.apiHost]
240
+ * @param {typeof fetch} [options.fetch]
241
+ * @param {() => number} [options.now] - seconds
242
+ * @param {(payload: string) => Promise<string>} [options.hasher]
243
+ */
244
+ export function createAlephAuthorizer(options = {}) {
245
+ const { owner, sign } = options;
246
+ const apiHost = options.apiHost || DEFAULT_ALEPH_API_HOST;
247
+ const hasher = options.hasher || sha256Hex;
248
+ const doFetch = options.fetch || globalThis.fetch;
249
+ if (!owner) throw new BackendError("INVALID_BACKEND", "createAlephAuthorizer needs the owner's address");
250
+ if (typeof sign !== "function") throw new BackendError("INVALID_BACKEND", "createAlephAuthorizer needs a `sign` function");
251
+ if (typeof doFetch !== "function") throw new BackendError("INVALID_BACKEND", "createAlephAuthorizer needs fetch");
252
+
253
+ const same = (a, b) => String(a).toLowerCase() === String(b).toLowerCase();
254
+
255
+ /** @returns {Promise<AlephAuthorization[]>} */
256
+ async function read() {
257
+ const response = await doFetch(`${apiHost}/api/v0/aggregates/${owner}.json?keys=${SECURITY}`);
258
+ if (response.status === 404) return [];
259
+ if (!response.ok) {
260
+ throw new BackendError("UNSUPPORTED", `Aleph did not answer the owner's permissions: ${response.status}`);
261
+ }
262
+ const body = await response.json().catch(() => ({}));
263
+ const list = body?.data?.[SECURITY]?.authorizations;
264
+ return Array.isArray(list) ? list : [];
265
+ }
266
+
267
+ /** @param {AlephAuthorization[]} authorizations */
268
+ async function write(authorizations) {
269
+ const unsigned = await buildAuthorizationMessage({ owner, authorizations, hasher, now: options.now?.() });
270
+ const signature = await sign(owner, signaturePayload(unsigned));
271
+ const response = await doFetch(`${apiHost}/api/v0/messages`, {
272
+ method: "POST",
273
+ headers: { "content-type": "application/json" },
274
+ body: JSON.stringify({
275
+ message: { ...unsigned, signature: signature.startsWith("0x") ? signature : `0x${signature}` },
276
+ sync: true,
277
+ }),
278
+ });
279
+ if (!response.ok && response.status !== 202) {
280
+ const detail = await response.text().catch(() => "");
281
+ throw new BackendError("UNSUPPORTED", `Aleph refused the authorization: ${response.status} ${detail.slice(0, 200)}`);
282
+ }
283
+ const body = await response.json().catch(() => ({}));
284
+ return { itemHash: unsigned.item_hash, status: body?.message_status ?? "pending", authorizations };
285
+ }
286
+
287
+ return {
288
+ read,
289
+ write,
290
+ /** Grant, or replace the grant of the same address. @param {AlephAuthorization} authorization */
291
+ async authorize(authorization) {
292
+ const others = (await read()).filter((a) => !same(a.address, authorization?.address));
293
+ return write([...others, authorization]);
294
+ },
295
+ /** Take one address's grant away; the others stay. @param {string} address */
296
+ async revoke(address) {
297
+ return write((await read()).filter((a) => !same(a.address, address)));
298
+ },
299
+ };
300
+ }
301
+
161
302
  export default createAlephPin;