@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,187 @@
1
+ /**
2
+ * @fileoverview Storage backend contract for OrbitDB Storage Bridge
3
+ *
4
+ * The backend is the volatile part of this library: Storacha switched off writes on
5
+ * 2026-05-15 and its endpoints were gone by September, while every line of block
6
+ * extraction, CID bridging and CAR packing around it kept working. This module is the
7
+ * seam that admission of that fact requires — one contract, several drivers, no single
8
+ * service that can take the library with it.
9
+ *
10
+ * Backends differ in who computes the CID, and the contract has to admit all three shapes:
11
+ *
12
+ * - **pin-by-CID** — we publish through Helia and the service fetches what is already
13
+ * ours, so hashes cannot drift. `pinCid()`, declared by `capabilities.pinByCid`.
14
+ * - **blob push** — we push a CAR and get an opaque handle back. `putBlob()`.
15
+ * - **file push** — the service hashes what we send. Only safe when what we send is a
16
+ * CAR, which turns it back into the case above.
17
+ *
18
+ * Capabilities are declared rather than discovered, so the bridge can pick a strategy and
19
+ * the conformance suite can skip what a driver never claimed to do.
20
+ *
21
+ * @author @NiKrause
22
+ * @see {@link ../../docs/STORAGE-BACKENDS.md} for the evaluation behind these distinctions
23
+ */
24
+
25
+ /**
26
+ * @typedef {object} BackendCapabilities
27
+ * @property {boolean} pinByCid - `pinCid()` is implemented: content is fetched from IPFS by CID
28
+ * @property {boolean} carImport - CAR files are unpacked by the service and the inner blocks indexed
29
+ * @property {boolean} preservesInnerCids - stored bytes come back under the CID we computed
30
+ * @property {boolean} browserSafeAuth - no shared secret has to reach the browser
31
+ * @property {boolean} delegation - access can be handed on, scoped and time-bounded
32
+ * @property {boolean} listing - `list()` is implemented
33
+ * @property {boolean} deletion - `remove()` is implemented
34
+ * @property {number} minBlobSize - smallest accepted upload in bytes; 0 when there is no minimum
35
+ */
36
+
37
+ /**
38
+ * @typedef {object} BackendHandle
39
+ * @property {string} id - what this backend needs to get the bytes back
40
+ * @property {string} backend - the `name` of the backend that issued it
41
+ * @property {string} [cid] - set when the id is, or carries, a content identifier
42
+ * @property {number} [size] - stored size in bytes, when the backend reports one
43
+ */
44
+
45
+ /**
46
+ * @typedef {object} StorageBackend
47
+ * @property {string} name
48
+ * @property {BackendCapabilities} capabilities
49
+ * @property {(bytes: Uint8Array, meta?: object) => Promise<BackendHandle>} putBlob
50
+ * @property {(handle: BackendHandle|string) => Promise<Uint8Array>} getBlob
51
+ * @property {((cid: string, meta?: object) => Promise<BackendHandle>)} [pinCid]
52
+ * @property {((options?: object) => Promise<BackendHandle[]>)} [list]
53
+ * @property {((handle: BackendHandle|string) => Promise<void>)} [remove]
54
+ * @property {(() => Promise<void>)} [close]
55
+ */
56
+
57
+ /** Capability defaults. A driver states what it can do; anything unstated is a no. */
58
+ export const DEFAULT_CAPABILITIES = Object.freeze({
59
+ pinByCid: false,
60
+ carImport: false,
61
+ preservesInnerCids: false,
62
+ browserSafeAuth: false,
63
+ delegation: false,
64
+ listing: false,
65
+ deletion: false,
66
+ minBlobSize: 0,
67
+ });
68
+
69
+ /** Methods every driver must provide. */
70
+ export const REQUIRED_METHODS = Object.freeze(["putBlob", "getBlob"]);
71
+
72
+ /** Optional methods, and the capability flag that must agree with each. */
73
+ export const OPTIONAL_METHODS = Object.freeze({
74
+ pinCid: "pinByCid",
75
+ list: "listing",
76
+ remove: "deletion",
77
+ });
78
+
79
+ /**
80
+ * Error raised by the contract itself, so a caller can tell "this backend cannot"
81
+ * from "this backend failed".
82
+ */
83
+ export class BackendError extends Error {
84
+ /**
85
+ * @param {string} code - UNSUPPORTED, TOO_SMALL, NOT_FOUND or INVALID_BACKEND
86
+ * @param {string} message
87
+ */
88
+ constructor(code, message) {
89
+ super(message);
90
+ this.name = "BackendError";
91
+ this.code = code;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Normalise a handle, so drivers can accept either the object they issued or a bare id.
97
+ *
98
+ * @param {BackendHandle|string} handle
99
+ * @returns {string}
100
+ */
101
+ export function handleId(handle) {
102
+ const id = typeof handle === "string" ? handle : handle?.id;
103
+ if (!id) {
104
+ throw new BackendError("NOT_FOUND", "A backend handle or id is required");
105
+ }
106
+ return id;
107
+ }
108
+
109
+ /**
110
+ * Validate a driver against the contract and return it hardened.
111
+ *
112
+ * The wrapper is thin on purpose — it fills in capability defaults, rejects a driver
113
+ * whose methods and flags disagree, and enforces `minBlobSize` before the bytes leave
114
+ * the process. That last one matters: Filecoin Onchain Cloud rejects anything under
115
+ * 127 bytes, and an OrbitDB block is routinely smaller, so this turns a vendor error
116
+ * arriving mid-backup into a contract error raised on the first block.
117
+ *
118
+ * @param {StorageBackend} backend
119
+ * @returns {StorageBackend}
120
+ */
121
+ export function defineBackend(backend) {
122
+ if (!backend || typeof backend.name !== "string" || !backend.name) {
123
+ throw new BackendError("INVALID_BACKEND", "A backend needs a name");
124
+ }
125
+
126
+ for (const method of REQUIRED_METHODS) {
127
+ if (typeof backend[method] !== "function") {
128
+ throw new BackendError(
129
+ "INVALID_BACKEND",
130
+ `Backend "${backend.name}" is missing ${method}()`,
131
+ );
132
+ }
133
+ }
134
+
135
+ const capabilities = Object.freeze({
136
+ ...DEFAULT_CAPABILITIES,
137
+ ...(backend.capabilities || {}),
138
+ });
139
+
140
+ for (const [method, flag] of Object.entries(OPTIONAL_METHODS)) {
141
+ const implemented = typeof backend[method] === "function";
142
+ if (implemented !== Boolean(capabilities[flag])) {
143
+ throw new BackendError(
144
+ "INVALID_BACKEND",
145
+ `Backend "${backend.name}" declares ${flag}=${capabilities[flag]} but ` +
146
+ `${implemented ? "implements" : "does not implement"} ${method}()`,
147
+ );
148
+ }
149
+ }
150
+
151
+ const putBlob = backend.putBlob.bind(backend);
152
+
153
+ return Object.freeze({
154
+ ...backend,
155
+ capabilities,
156
+ putBlob: async (bytes, meta) => {
157
+ if (!(bytes instanceof Uint8Array)) {
158
+ throw new BackendError(
159
+ "INVALID_BACKEND",
160
+ "putBlob expects a Uint8Array",
161
+ );
162
+ }
163
+ if (bytes.length < capabilities.minBlobSize) {
164
+ throw new BackendError(
165
+ "TOO_SMALL",
166
+ `Backend "${backend.name}" needs at least ${capabilities.minBlobSize} bytes, got ${bytes.length}. ` +
167
+ "Pack blocks into a CAR before uploading.",
168
+ );
169
+ }
170
+ return putBlob(bytes, meta);
171
+ },
172
+ });
173
+ }
174
+
175
+ /**
176
+ * Raise a consistent error for something a driver deliberately does not do.
177
+ *
178
+ * @param {string} name - backend name
179
+ * @param {string} operation
180
+ * @returns {never}
181
+ */
182
+ export function unsupported(name, operation) {
183
+ throw new BackendError(
184
+ "UNSUPPORTED",
185
+ `Backend "${name}" does not support ${operation}`,
186
+ );
187
+ }