@le-space/orbitdb-storage-bridge 0.10.0 → 0.11.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 +23 -5
- package/lib/backends/choose.js +145 -0
- package/lib/backends/mirror.js +234 -0
- package/lib/backends/resolve.js +10 -0
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
[](https://nodejs.org/)
|
|
8
|
-
[](https://github.com/NiKrause
|
|
8
|
+
[](https://github.com/NiKrause/orbitdb-storage-bridge/actions/workflows/ci.yml)
|
|
9
|
+
[](https://github.com/NiKrause/orbitdb-storage-bridge/actions/workflows/ci.yml)
|
|
10
10
|
[](https://www.npmjs.com/package/@le-space/orbitdb-storage-bridge)
|
|
11
11
|
|
|
12
12
|
> [!NOTE]
|
|
@@ -157,7 +157,7 @@ The project includes **Svelte components** for browser-based demos and integrati
|
|
|
157
157
|
## Roadmap
|
|
158
158
|
|
|
159
159
|
> Being re-based on a backend interface instead of a single vendor — the plan is
|
|
160
|
-
> [issue 54](https://github.com/NiKrause
|
|
160
|
+
> [issue 54](https://github.com/NiKrause/orbitdb-storage-bridge/issues/54), not here. The WebAuthn/varsig items below survive
|
|
161
161
|
> unchanged; the Storacha-named ones become backend-agnostic.
|
|
162
162
|
|
|
163
163
|
- [ ] 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.
|
|
@@ -169,11 +169,11 @@ The project includes **Svelte components** for browser-based demos and integrati
|
|
|
169
169
|
- [ ] 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.
|
|
170
170
|
- [ ] After each backup, write a small pointer record (JSON) that stores the latest metadata CID, CAR CID, and last heads (block CID).
|
|
171
171
|
- [ ] Store that pointer in a user-controlled place (local storage, QR/share link, WebAuthN largetBlog extension or file download).
|
|
172
|
-
- [ ] v0.5.0 (Feb 2026): OrbitDB CustomStorage (StorachaStorage) ([issue 23](https://github.com/NiKrause
|
|
172
|
+
- [ ] v0.5.0 (Feb 2026): OrbitDB CustomStorage (StorachaStorage) ([issue 23](https://github.com/NiKrause/orbitdb-storage-bridge/issues/23)).
|
|
173
173
|
- [ ] 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
|
|
174
174
|
- [ ] v0.6.1 (Mar 2026): WebAuthN + SimpleEncryption example that uses WebAuthN+PRF key material for encrypted backups and restore.
|
|
175
175
|
- [ ] v0.7.0 (Apr 2026): WebAuthN + OrbitDB AccessController (store a UCAN instead of only a DID for admin/write access).
|
|
176
|
-
- [ ] Alice (authenticated via UCAN or Storacha credentials) can delegate/revoke access for Bob with custom/default capabilities ([issue 16](https://github.com/NiKrause
|
|
176
|
+
- [ ] 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/).
|
|
177
177
|
- [ ] v0.7.1 (May 2026): Storacha Backup & Restore Svelte widget with WebAuthN-varsig UCAN signing/verification (Ed25519/P-256).
|
|
178
178
|
- [ ] v0.7.2 (May 2026): Storacha Backup & Restore React widget with WebAuthN-varsig UCAN signing/verification (Ed25519/P-256).
|
|
179
179
|
- [ ] v0.7.3 (May 2026): Storacha Backup & Restore React widget with WebAuthN-varsig UCAN delegation (Ed25519/P-256).
|
|
@@ -224,6 +224,24 @@ The scripts written against Storacha's space and UCAN model are in
|
|
|
224
224
|
[`examples/storacha/`](examples/storacha/README.md). None of them runs end to
|
|
225
225
|
end since the uploads stopped, and the README there says what replaced each.
|
|
226
226
|
|
|
227
|
+
### In a browser: a database back on a device that has nothing
|
|
228
|
+
|
|
229
|
+
[**funkpost's recovery page**](https://nikrause.github.io/funkpost/recovery/) runs the whole
|
|
230
|
+
[recovery procedure](docs/RECOVERY-ON-A-SECOND-DEVICE.md) in a phone's browser, with a
|
|
231
|
+
security key and nothing else ([source](https://github.com/NiKrause/funkpost/tree/main/examples/recovery)):
|
|
232
|
+
|
|
233
|
+
1. **the key gives the identity** — the DID, and a signing key derived from its PRF output,
|
|
234
|
+
the same on every device;
|
|
235
|
+
2. **a list** is made and written to;
|
|
236
|
+
3. **`dehydrate`** backs it up to Aleph as a CAR, and publishes an IPNS pointer under a name
|
|
237
|
+
the key derives;
|
|
238
|
+
4. **on another device** — or the same one, wiped — the same key finds the pointer, **`hydrate`**
|
|
239
|
+
brings the list back, and the list takes new entries, because the writer is the same.
|
|
240
|
+
|
|
241
|
+
On 21 September 2026 it ran that way on two phones: a Galaxy Fold 5 backed up and was reset,
|
|
242
|
+
and a Galaxy A57 with the same key brought the list back and wrote to it. The page says at
|
|
243
|
+
every step which service it contacts; the technical details sit behind one button.
|
|
244
|
+
|
|
227
245
|
### Svelte Components
|
|
228
246
|
|
|
229
247
|
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.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview One backend from a plain choice — a name and, where a service
|
|
3
|
+
* needs one, a key.
|
|
4
|
+
*
|
|
5
|
+
* `resolveBackend` understands a ready backend or Storacha credentials, which is
|
|
6
|
+
* what a Node caller has. A page has something smaller and less trusting: a
|
|
7
|
+
* reader ticked *Lighthouse* and pasted their own key, and nothing else is
|
|
8
|
+
* known. This turns that into a driver.
|
|
9
|
+
*
|
|
10
|
+
* ## Every vendor module is imported lazily, and that is the point
|
|
11
|
+
*
|
|
12
|
+
* A page that chose Aleph must not ship the Pinata and Lighthouse drivers, and
|
|
13
|
+
* a page that chose none of them must ship no vendor code at all. Each branch
|
|
14
|
+
* therefore `import()`s exactly what it builds — the same reason
|
|
15
|
+
* `resolveBackend` loads Storacha only when it builds one, where the module
|
|
16
|
+
* costs about 88 kB gzipped.
|
|
17
|
+
*
|
|
18
|
+
* ## Several choices are a mirror, not a loop in the caller
|
|
19
|
+
*
|
|
20
|
+
* `kind: ["aleph", "lighthouse"]` builds both and returns
|
|
21
|
+
* {@link ../backends/mirror.js createMirrorBackend} over them, so the caller's
|
|
22
|
+
* code is the same whether one service was ticked or three.
|
|
23
|
+
*
|
|
24
|
+
* ## What it refuses
|
|
25
|
+
*
|
|
26
|
+
* A key that is missing is refused here, with the name of the service in the
|
|
27
|
+
* message, rather than at the first upload — a page can then keep the button
|
|
28
|
+
* disabled and say why. Nothing in this module reads an environment variable on
|
|
29
|
+
* a page's behalf: in a browser there is no environment, and a driver that
|
|
30
|
+
* silently finds a key somewhere else is a driver nobody can reason about. The
|
|
31
|
+
* per-driver fallbacks to `process.env` stay where they are, for Node.
|
|
32
|
+
*
|
|
33
|
+
* @author @NiKrause
|
|
34
|
+
* @requires ./types.js - the contract every branch returns
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { BackendError } from "./types.js";
|
|
38
|
+
|
|
39
|
+
/** The names a caller may ask for. */
|
|
40
|
+
export const BACKEND_KINDS = Object.freeze([
|
|
41
|
+
"aleph",
|
|
42
|
+
"pinata",
|
|
43
|
+
"lighthouse",
|
|
44
|
+
"storacha",
|
|
45
|
+
"memory",
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build a backend from a choice.
|
|
50
|
+
*
|
|
51
|
+
* @param {object} choice
|
|
52
|
+
* @param {string|string[]} choice.kind - one name, or several for a mirror
|
|
53
|
+
* @param {string} [choice.jwt] - Pinata
|
|
54
|
+
* @param {() => Promise<string>} [choice.getUploadUrl] - Pinata, without a secret in the page
|
|
55
|
+
* @param {string} [choice.apiKey] - Lighthouse
|
|
56
|
+
* @param {"shared"|"user"} [choice.keyOwnership] - Lighthouse: whose key it is,
|
|
57
|
+
* which is what decides `browserSafeAuth`
|
|
58
|
+
* @param {string[]} [choice.gateways] - retrieval gateways, where the driver takes them
|
|
59
|
+
* @param {object} [choice.options] - passed through to the driver, for anything
|
|
60
|
+
* this signature does not name
|
|
61
|
+
* @param {"one"|"all"} [choice.require] - for several kinds: how many must accept a write
|
|
62
|
+
* @returns {Promise<import("./types.js").StorageBackend>}
|
|
63
|
+
*/
|
|
64
|
+
export async function createBackendFromChoice(choice = {}) {
|
|
65
|
+
const kinds = Array.isArray(choice.kind) ? choice.kind : [choice.kind];
|
|
66
|
+
const wanted = kinds.filter(Boolean);
|
|
67
|
+
|
|
68
|
+
if (wanted.length === 0) {
|
|
69
|
+
throw new BackendError(
|
|
70
|
+
"INVALID_BACKEND",
|
|
71
|
+
`Pick a backend: ${BACKEND_KINDS.join(", ")}`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
for (const kind of wanted) {
|
|
75
|
+
if (!BACKEND_KINDS.includes(kind)) {
|
|
76
|
+
throw new BackendError(
|
|
77
|
+
"INVALID_BACKEND",
|
|
78
|
+
`Unknown backend "${kind}" — pick one of ${BACKEND_KINDS.join(", ")}`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (wanted.length > 1) {
|
|
84
|
+
const backends = [];
|
|
85
|
+
for (const kind of wanted) {
|
|
86
|
+
backends.push(await createBackendFromChoice({ ...choice, kind }));
|
|
87
|
+
}
|
|
88
|
+
const { createMirrorBackend } = await import("./mirror.js");
|
|
89
|
+
return createMirrorBackend(backends, { require: choice.require ?? "one" });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const [kind] = wanted;
|
|
93
|
+
const gateways = choice.gateways ? { gateways: choice.gateways } : {};
|
|
94
|
+
const extra = choice.options ?? {};
|
|
95
|
+
|
|
96
|
+
if (kind === "aleph") {
|
|
97
|
+
const { createAlephBackend } = await import("./aleph.js");
|
|
98
|
+
return createAlephBackend({ ...gateways, ...extra });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (kind === "pinata") {
|
|
102
|
+
if (
|
|
103
|
+
!choice.jwt &&
|
|
104
|
+
!choice.getUploadUrl &&
|
|
105
|
+
!extra.jwt &&
|
|
106
|
+
!extra.getUploadUrl
|
|
107
|
+
) {
|
|
108
|
+
throw new BackendError(
|
|
109
|
+
"INVALID_BACKEND",
|
|
110
|
+
"Pinata needs a JWT, or a getUploadUrl function that mints a presigned URL",
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const { createPinataBackend } = await import("./pinata.js");
|
|
114
|
+
return createPinataBackend({
|
|
115
|
+
...(choice.jwt ? { jwt: choice.jwt } : {}),
|
|
116
|
+
...(choice.getUploadUrl ? { getUploadUrl: choice.getUploadUrl } : {}),
|
|
117
|
+
...gateways,
|
|
118
|
+
...extra,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (kind === "lighthouse") {
|
|
123
|
+
if (!choice.apiKey && !extra.apiKey) {
|
|
124
|
+
throw new BackendError("INVALID_BACKEND", "Lighthouse needs an apiKey");
|
|
125
|
+
}
|
|
126
|
+
const { createLighthouseBackend } = await import("./lighthouse.js");
|
|
127
|
+
return createLighthouseBackend({
|
|
128
|
+
...(choice.apiKey ? { apiKey: choice.apiKey } : {}),
|
|
129
|
+
// Whose key it is decides browserSafeAuth, and a page should say which it got.
|
|
130
|
+
keyOwnership: choice.keyOwnership ?? "shared",
|
|
131
|
+
...gateways,
|
|
132
|
+
...extra,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (kind === "memory") {
|
|
137
|
+
const { createMemoryBackend } = await import("./memory.js");
|
|
138
|
+
return createMemoryBackend(extra);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const { resolveBackend } = await import("./resolve.js");
|
|
142
|
+
return resolveBackend({ ...choice, ...extra, kind: undefined });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export default createBackendFromChoice;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview One backup, several services: the mirror backend.
|
|
3
|
+
*
|
|
4
|
+
* A page that wants its backup on Aleph *and* on Lighthouse had to call both
|
|
5
|
+
* and reconcile two handles itself. This does that once, behind the same
|
|
6
|
+
* contract every other driver keeps, so `dehydrate`, `backupDatabaseCAR` and
|
|
7
|
+
* `restoreFromCID` need to know nothing about it.
|
|
8
|
+
*
|
|
9
|
+
* ## What a mirror promises, and what it refuses to
|
|
10
|
+
*
|
|
11
|
+
* **A partial write is reported, never swallowed.** By default a write
|
|
12
|
+
* succeeds when at least one service took it — a backup that fails because the
|
|
13
|
+
* third service was down would be worse than useless — but the handle then
|
|
14
|
+
* names who holds it and who refused, and the caller decides what to say. Pass
|
|
15
|
+
* `require: "all"` when a copy everywhere is the point of the exercise.
|
|
16
|
+
*
|
|
17
|
+
* **A read asks in order and stops at the first answer.** Not a race: a race
|
|
18
|
+
* spends every service's bandwidth on every read, and on a gateway that bills
|
|
19
|
+
* by request that is somebody's money. The order is the order the backends
|
|
20
|
+
* were given, so "the fast one first" is the caller's decision to make.
|
|
21
|
+
*
|
|
22
|
+
* **Capabilities are the honest composition, not the flattering one.**
|
|
23
|
+
* `browserSafeAuth` is true only when *every* service is safe to hand a page,
|
|
24
|
+
* because the weakest one decides what a page leaks. `preservesInnerCids` and
|
|
25
|
+
* `carImport` likewise. `minBlobSize` is the largest of them, because a blob
|
|
26
|
+
* has to clear the strictest door. `pinByCid` is the exception: it is true when
|
|
27
|
+
* *any* service can pin, and `pinCid()` then asks exactly those.
|
|
28
|
+
*
|
|
29
|
+
* **It does not list and it does not delete.** Both questions have no single
|
|
30
|
+
* honest answer across services — a listing would be a union with duplicates,
|
|
31
|
+
* and a deletion that half succeeds leaves a copy behind while reporting
|
|
32
|
+
* success. A caller that wants either can ask the service it means.
|
|
33
|
+
*
|
|
34
|
+
* @author @NiKrause
|
|
35
|
+
* @requires ./types.js - the contract this keeps
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import {
|
|
39
|
+
defineBackend,
|
|
40
|
+
handleId,
|
|
41
|
+
BackendError,
|
|
42
|
+
DEFAULT_CAPABILITIES,
|
|
43
|
+
} from "./types.js";
|
|
44
|
+
import { logger } from "../logger.js";
|
|
45
|
+
|
|
46
|
+
/** The strictest door decides; an unstated minimum is no minimum. */
|
|
47
|
+
const largestMinimum = (backends) =>
|
|
48
|
+
backends.reduce(
|
|
49
|
+
(largest, backend) =>
|
|
50
|
+
Math.max(largest, backend.capabilities?.minBlobSize ?? 0),
|
|
51
|
+
0,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const everyOne = (backends, flag) =>
|
|
55
|
+
backends.every((backend) => Boolean(backend.capabilities?.[flag]));
|
|
56
|
+
const anyOne = (backends, flag) =>
|
|
57
|
+
backends.some((backend) => Boolean(backend.capabilities?.[flag]));
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Write one backup to several services and read it back from whichever answers.
|
|
61
|
+
*
|
|
62
|
+
* @param {import("./types.js").StorageBackend[]} backends - two or more drivers,
|
|
63
|
+
* in the order reads should try them
|
|
64
|
+
* @param {object} [options]
|
|
65
|
+
* @param {"one"|"all"} [options.require="one"] - how many services must accept a
|
|
66
|
+
* write for it to count as one
|
|
67
|
+
* @param {string} [options.name] - what the mirror calls itself; the default
|
|
68
|
+
* names its members, because a log line saying "mirror" says nothing
|
|
69
|
+
* @returns {import("./types.js").StorageBackend}
|
|
70
|
+
*/
|
|
71
|
+
export function createMirrorBackend(backends = [], options = {}) {
|
|
72
|
+
const members = backends.filter(Boolean);
|
|
73
|
+
if (members.length < 2) {
|
|
74
|
+
throw new BackendError(
|
|
75
|
+
"INVALID_BACKEND",
|
|
76
|
+
"createMirrorBackend needs at least two backends; one backend is not a mirror",
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const require_ = options.require ?? "one";
|
|
81
|
+
if (require_ !== "one" && require_ !== "all") {
|
|
82
|
+
throw new BackendError(
|
|
83
|
+
"INVALID_BACKEND",
|
|
84
|
+
`require must be "one" or "all", not ${require_}`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const name =
|
|
89
|
+
options.name ||
|
|
90
|
+
`mirror(${members.map((backend) => backend.name).join("+")})`;
|
|
91
|
+
const pinners = members.filter(
|
|
92
|
+
(backend) => typeof backend.pinCid === "function",
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
/** Run one operation against every member, keeping which of them said what. */
|
|
96
|
+
const acrossAll = async (operation) => {
|
|
97
|
+
const settled = await Promise.allSettled(
|
|
98
|
+
members.map((backend) => operation(backend)),
|
|
99
|
+
);
|
|
100
|
+
const copies = [];
|
|
101
|
+
const failures = [];
|
|
102
|
+
settled.forEach((result, index) => {
|
|
103
|
+
const backend = members[index];
|
|
104
|
+
if (result.status === "fulfilled") {
|
|
105
|
+
copies.push({ backend: backend.name, ...result.value });
|
|
106
|
+
} else {
|
|
107
|
+
failures.push({
|
|
108
|
+
backend: backend.name,
|
|
109
|
+
code: result.reason?.code ?? "FAILED",
|
|
110
|
+
message: result.reason?.message ?? String(result.reason),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
return { copies, failures };
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* One handle for the copies. The id is the CID when every service agreed on
|
|
119
|
+
* one — which is the normal case, since the bytes decide it — and the first
|
|
120
|
+
* success otherwise, so a handle is always usable somewhere.
|
|
121
|
+
*/
|
|
122
|
+
const mergeHandles = ({ copies, failures }, what) => {
|
|
123
|
+
if (copies.length === 0) {
|
|
124
|
+
throw new BackendError(
|
|
125
|
+
failures[0]?.code === "TOO_SMALL" ? "TOO_SMALL" : "FAILED",
|
|
126
|
+
`${name}: no service accepted ${what} — ${failures
|
|
127
|
+
.map((failure) => `${failure.backend}: ${failure.message}`)
|
|
128
|
+
.join("; ")}`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (require_ === "all" && failures.length > 0) {
|
|
132
|
+
throw new BackendError(
|
|
133
|
+
"FAILED",
|
|
134
|
+
`${name}: ${failures.length} of ${members.length} services refused ${what} and require is "all" — ${failures
|
|
135
|
+
.map((failure) => `${failure.backend}: ${failure.message}`)
|
|
136
|
+
.join("; ")}`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
if (failures.length > 0) {
|
|
140
|
+
logger.warn(
|
|
141
|
+
`⚠️ ${name}: ${copies.length} of ${members.length} services hold ${what}; ` +
|
|
142
|
+
failures
|
|
143
|
+
.map((failure) => `${failure.backend} refused (${failure.code})`)
|
|
144
|
+
.join(", "),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const cids = new Set(copies.map((copy) => copy.cid).filter(Boolean));
|
|
149
|
+
const agreed = cids.size === 1 ? [...cids][0] : null;
|
|
150
|
+
return {
|
|
151
|
+
id: agreed ?? copies[0].id,
|
|
152
|
+
...(agreed ? { cid: agreed } : {}),
|
|
153
|
+
backend: name,
|
|
154
|
+
...(copies[0].size != null ? { size: copies[0].size } : {}),
|
|
155
|
+
...(copies[0].name ? { name: copies[0].name } : {}),
|
|
156
|
+
copies,
|
|
157
|
+
...(failures.length > 0 ? { failures } : {}),
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/** A member's own id for this handle, since only the CID is shared. */
|
|
162
|
+
const idFor = (backend, handle) => {
|
|
163
|
+
const copy =
|
|
164
|
+
typeof handle === "object" && handle?.copies
|
|
165
|
+
? handle.copies.find((entry) => entry.backend === backend.name)
|
|
166
|
+
: null;
|
|
167
|
+
return copy?.id ?? handleId(handle);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const mirror = {
|
|
171
|
+
name,
|
|
172
|
+
/** The members, in read order — a caller that wants one service can reach it. */
|
|
173
|
+
backends: members,
|
|
174
|
+
capabilities: {
|
|
175
|
+
...DEFAULT_CAPABILITIES,
|
|
176
|
+
pinByCid: pinners.length > 0 && anyOne(members, "pinByCid"),
|
|
177
|
+
carImport: everyOne(members, "carImport"),
|
|
178
|
+
preservesInnerCids: everyOne(members, "preservesInnerCids"),
|
|
179
|
+
browserSafeAuth: everyOne(members, "browserSafeAuth"),
|
|
180
|
+
delegation: everyOne(members, "delegation"),
|
|
181
|
+
listing: false,
|
|
182
|
+
deletion: false,
|
|
183
|
+
minBlobSize: largestMinimum(members),
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
putBlob: async (bytes, meta = {}) =>
|
|
187
|
+
mergeHandles(
|
|
188
|
+
await acrossAll((backend) => backend.putBlob(bytes, meta)),
|
|
189
|
+
"the blob",
|
|
190
|
+
),
|
|
191
|
+
|
|
192
|
+
getBlob: async (handle) => {
|
|
193
|
+
const reasons = [];
|
|
194
|
+
for (const backend of members) {
|
|
195
|
+
try {
|
|
196
|
+
return await backend.getBlob(idFor(backend, handle));
|
|
197
|
+
} catch (error) {
|
|
198
|
+
reasons.push(`${backend.name}: ${error?.message ?? error}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
throw new BackendError(
|
|
202
|
+
"NOT_FOUND",
|
|
203
|
+
`${name}: no service returned ${handleId(handle)} — ${reasons.join("; ")}`,
|
|
204
|
+
);
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
if (mirror.capabilities.pinByCid) {
|
|
209
|
+
/** Only the services that can pin are asked; the others have nothing to say. */
|
|
210
|
+
mirror.pinCid = async (cid, meta = {}) => {
|
|
211
|
+
const settled = await Promise.allSettled(
|
|
212
|
+
pinners.map((backend) => backend.pinCid(cid, meta)),
|
|
213
|
+
);
|
|
214
|
+
const copies = [];
|
|
215
|
+
const failures = [];
|
|
216
|
+
settled.forEach((result, index) => {
|
|
217
|
+
const backend = pinners[index];
|
|
218
|
+
if (result.status === "fulfilled")
|
|
219
|
+
copies.push({ backend: backend.name, ...result.value });
|
|
220
|
+
else
|
|
221
|
+
failures.push({
|
|
222
|
+
backend: backend.name,
|
|
223
|
+
code: result.reason?.code ?? "FAILED",
|
|
224
|
+
message: result.reason?.message ?? String(result.reason),
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
return mergeHandles({ copies, failures }, `the pin for ${cid}`);
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return defineBackend(mirror);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export default createMirrorBackend;
|
package/lib/backends/resolve.js
CHANGED
|
@@ -20,6 +20,9 @@ import { logger } from "../logger.js";
|
|
|
20
20
|
*
|
|
21
21
|
* @param {Object} [config] - call options
|
|
22
22
|
* @param {Object} [config.backend] - any driver implementing the backend contract
|
|
23
|
+
* @param {string|string[]} [config.kind] - a backend by name, or several for a
|
|
24
|
+
* mirror, with the credentials each needs. What a page has, since a reader
|
|
25
|
+
* ticks a service and pastes a key rather than handing over a driver.
|
|
23
26
|
* @param {Object} [config.ucanClient] - a Storacha client authorised over UCAN
|
|
24
27
|
* @param {string} [config.spaceDID]
|
|
25
28
|
* @param {string} [config.storachaKey] - falls back to STORACHA_KEY
|
|
@@ -35,6 +38,13 @@ export async function resolveBackend(config = {}) {
|
|
|
35
38
|
return config.backend;
|
|
36
39
|
}
|
|
37
40
|
|
|
41
|
+
// A name and a key: loaded here so that one decision stays one decision, and
|
|
42
|
+
// lazily so a caller that never names a vendor never bundles one.
|
|
43
|
+
if (config.kind) {
|
|
44
|
+
const { createBackendFromChoice } = await import("./choose.js");
|
|
45
|
+
return createBackendFromChoice(config);
|
|
46
|
+
}
|
|
47
|
+
|
|
38
48
|
const gateways = config.gateways;
|
|
39
49
|
|
|
40
50
|
if (config.ucanClient) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@le-space/orbitdb-storage-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Back up, restore and replicate OrbitDB databases through pluggable storage backends, with hash and identity preservation",
|
|
5
5
|
"main": "lib/orbitdb-storacha-bridge.js",
|
|
6
6
|
"svelte": "dist/components/",
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
"./backends/aleph-pin": "./lib/backends/aleph-pin.js",
|
|
24
24
|
"./backends/pinata": "./lib/backends/pinata.js",
|
|
25
25
|
"./backends/lighthouse": "./lib/backends/lighthouse.js",
|
|
26
|
+
"./backends/mirror": "./lib/backends/mirror.js",
|
|
27
|
+
"./backends/choose": "./lib/backends/choose.js",
|
|
28
|
+
"./backends/resolve": "./lib/backends/resolve.js",
|
|
26
29
|
"./memory-courier": "./lib/memory-courier.js",
|
|
27
30
|
"./StorachaIntegration.svelte": "./dist/components/StorachaIntegration.svelte",
|
|
28
31
|
"./StorachaAuth.svelte": "./dist/components/StorachaAuth.svelte",
|