@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,325 @@
1
+ /**
2
+ * Storacha Authentication and Management Functions for Demo
3
+ *
4
+ * Provides various authentication methods for Storacha:
5
+ * - Email-based account creation
6
+ * - Key/Proof credentials
7
+ * - UCAN delegation
8
+ * - Seed phrase derived identities
9
+ */
10
+
11
+ import * as Client from "@storacha/client";
12
+ import { StoreMemory } from "@storacha/client/stores/memory";
13
+ import { Signer } from "@storacha/client/principal/ed25519";
14
+ import * as Proof from "@storacha/client/proof";
15
+ import * as Delegation from "@ucanto/core/delegation";
16
+ import * as ed25519 from "@ucanto/principal/ed25519";
17
+ import { generateMnemonic, mnemonicToSeedSync } from "@scure/bip39";
18
+ import { wordlist as english } from "@scure/bip39/wordlists/english";
19
+ import { createHash } from "crypto";
20
+ import { logger } from '../../lib/logger.js'
21
+
22
+ /**
23
+ * Initialize Storacha client with key/proof credentials
24
+ */
25
+ export async function initializeStorachaClient(storachaKey, storachaProof) {
26
+ try {
27
+ logger.info("🔐 Initializing Storacha client with credentials...");
28
+
29
+ const principal = Signer.parse(storachaKey);
30
+ const store = new StoreMemory();
31
+ const client = await Client.create({ principal, store });
32
+
33
+ const proof = await Proof.parse(storachaProof);
34
+ const space = await client.addSpace(proof);
35
+ await client.setCurrentSpace(space.did());
36
+
37
+ logger.info(`✅ Storacha client initialized with space: ${space.did()}`);
38
+ return client;
39
+ } catch (error) {
40
+ logger.error("❌ Failed to initialize Storacha client:", error);
41
+ throw error;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Initialize Storacha client with UCAN delegation
47
+ */
48
+ export async function initializeStorachaClientWithUCAN(
49
+ ucanToken,
50
+ recipientKey,
51
+ ) {
52
+ try {
53
+ logger.info("🔐 Initializing Storacha client with UCAN...");
54
+
55
+ // Parse recipient identity from JSON archive
56
+ const recipientKeyData = JSON.parse(recipientKey);
57
+ const fixedArchive = {
58
+ id: recipientKeyData.id,
59
+ keys: {
60
+ [recipientKeyData.id]: new Uint8Array(
61
+ Object.values(recipientKeyData.keys[recipientKeyData.id]),
62
+ ),
63
+ },
64
+ };
65
+
66
+ const recipientPrincipal = Signer.from(fixedArchive);
67
+ const store = new StoreMemory();
68
+ const client = await Client.create({
69
+ principal: recipientPrincipal,
70
+ store,
71
+ });
72
+
73
+ // Parse delegation token
74
+ const delegationBytes = Buffer.from(ucanToken, "base64");
75
+ const delegation = await Delegation.extract(delegationBytes);
76
+
77
+ if (!delegation.ok) {
78
+ throw new Error("Failed to extract delegation from token");
79
+ }
80
+
81
+ // Add space using delegation
82
+ const space = await client.addSpace(delegation.ok);
83
+ await client.setCurrentSpace(space.did());
84
+
85
+ logger.info(
86
+ `✅ Storacha client initialized with UCAN space: ${space.did()}`,
87
+ );
88
+ return client;
89
+ } catch (error) {
90
+ logger.error("❌ Failed to initialize Storacha client with UCAN:", error);
91
+ throw error;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Convert 64-bit seed to 32-bit seed (same as deContact)
97
+ */
98
+ function convertTo32BitSeed(origSeed) {
99
+ const hash = createHash("sha256");
100
+ hash.update(Buffer.from(origSeed, "hex"));
101
+ return hash.digest();
102
+ }
103
+
104
+ // Convert Uint8Array to hex (browser-safe)
105
+ function toHex(u8) {
106
+ return Array.from(u8)
107
+ .map((b) => b.toString(16).padStart(2, "0"))
108
+ .join("");
109
+ }
110
+
111
+ /**
112
+ * Generate master seed from mnemonic
113
+ */
114
+ function generateMasterSeed(mnemonicSeedphrase, password = "password") {
115
+ return toHex(mnemonicToSeedSync(mnemonicSeedphrase, password));
116
+ }
117
+
118
+ /**
119
+ * Create Storacha identity from seed phrase
120
+ */
121
+ export async function createStorachaIdentityFromSeed(
122
+ seedPhrase,
123
+ password = "password",
124
+ derivationIndex = 1,
125
+ ) {
126
+ try {
127
+ logger.info("🌱 Creating Storacha identity from seed phrase...");
128
+
129
+ const masterSeed = generateMasterSeed(seedPhrase, password);
130
+ const seed32 = convertTo32BitSeed(masterSeed);
131
+
132
+ // Create derived seed for Storacha (different from OrbitDB)
133
+ const derivedSeed = new Uint8Array(32);
134
+ derivedSeed.set(seed32);
135
+ derivedSeed[31] ^= derivationIndex; // Derivation index
136
+
137
+ const principal = await ed25519.derive(derivedSeed);
138
+ const signer = Signer.from(principal.toArchive());
139
+
140
+ logger.info(`✅ Storacha identity created: ${signer.did()}`);
141
+ return {
142
+ principal: signer,
143
+ did: signer.did(),
144
+ privateKey: signer.toArchive(),
145
+ seedPhrase,
146
+ masterSeed,
147
+ };
148
+ } catch (error) {
149
+ logger.error("❌ Failed to create Storacha identity from seed:", error);
150
+ throw error;
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Initialize Storacha client with seed-derived identity and delegation
156
+ */
157
+ export async function initializeStorachaClientWithSeed(
158
+ seedPhrase,
159
+ password = "password",
160
+ delegationToken = null,
161
+ ) {
162
+ try {
163
+ logger.info("🌱 Initializing Storacha client with seed phrase...");
164
+
165
+ const identity = await createStorachaIdentityFromSeed(seedPhrase, password);
166
+ const store = new StoreMemory();
167
+ const client = await Client.create({
168
+ principal: identity.principal,
169
+ store,
170
+ });
171
+
172
+ if (delegationToken) {
173
+ // Use provided delegation
174
+ const delegationBytes = Buffer.from(delegationToken, "base64");
175
+ const delegation = await Delegation.extract(delegationBytes);
176
+
177
+ if (!delegation.ok) {
178
+ throw new Error("Failed to extract delegation from token");
179
+ }
180
+
181
+ const space = await client.addSpace(delegation.ok);
182
+ await client.setCurrentSpace(space.did());
183
+
184
+ logger.info(
185
+ `✅ Storacha client initialized with seed + delegation: ${space.did()}`,
186
+ );
187
+ } else {
188
+ logger.info(
189
+ `✅ Storacha client initialized with seed identity: ${identity.did}`,
190
+ );
191
+ logger.info("⚠️ No delegation provided - limited functionality");
192
+ }
193
+
194
+ return { client, identity };
195
+ } catch (error) {
196
+ logger.error("❌ Failed to initialize Storacha client with seed:", error);
197
+ throw error;
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Create new Storacha account with email (placeholder - requires w3 CLI integration)
203
+ */
204
+ export async function createStorachaAccount(email) {
205
+ try {
206
+ logger.info(`📧 Creating Storacha account for: ${email}`);
207
+
208
+ // This is a placeholder - in reality, this would integrate with w3 CLI
209
+ // or require the user to manually create an account
210
+ return {
211
+ success: false,
212
+ message:
213
+ "Account creation requires manual setup. Please visit web3.storage to create an account and get your credentials.",
214
+ error: "Manual setup required",
215
+ };
216
+ } catch (error) {
217
+ logger.error("❌ Failed to create Storacha account:", error);
218
+ return {
219
+ success: false,
220
+ error: error.message,
221
+ };
222
+ }
223
+ }
224
+
225
+ /**
226
+ * List spaces for authenticated client
227
+ */
228
+ export async function listSpaces(client) {
229
+ try {
230
+ logger.info("📋 Listing Storacha spaces...");
231
+
232
+ const spaces = [];
233
+
234
+ // Try to get current space
235
+ const currentSpace = client.currentSpace();
236
+ if (currentSpace) {
237
+ spaces.push({
238
+ did: currentSpace.did(),
239
+ name: currentSpace.name || "Current Space",
240
+ });
241
+ }
242
+
243
+ // Try to get accounts and their spaces
244
+ try {
245
+ const accounts = client.accounts();
246
+ for (const account of accounts) {
247
+ const accountSpaces = account.spaces();
248
+ for (const space of accountSpaces) {
249
+ const spaceInfo = {
250
+ did: space.did(),
251
+ name: space.name || "Unnamed Space",
252
+ };
253
+
254
+ // Avoid duplicates
255
+ if (!spaces.find((s) => s.did === spaceInfo.did)) {
256
+ spaces.push(spaceInfo);
257
+ }
258
+ }
259
+ }
260
+ } catch (accountError) {
261
+ logger.warn("⚠️ Could not list account spaces:", accountError.message);
262
+ }
263
+
264
+ logger.info(`✅ Found ${spaces.length} spaces`);
265
+ return spaces;
266
+ } catch (error) {
267
+ logger.error("❌ Failed to list spaces:", error);
268
+ throw error;
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Create new space
274
+ */
275
+ export async function createSpace(client, spaceName) {
276
+ try {
277
+ logger.info(`🆕 Creating space: ${spaceName}`);
278
+
279
+ // This requires account-based authentication
280
+ const accounts = client.accounts();
281
+ if (accounts.length === 0) {
282
+ throw new Error(
283
+ "No accounts found - space creation requires account-based authentication",
284
+ );
285
+ }
286
+
287
+ const account = accounts[0];
288
+ const space = await account.createSpace(spaceName);
289
+
290
+ logger.info(`✅ Space created: ${space.did()}`);
291
+ return {
292
+ success: true,
293
+ space: {
294
+ did: space.did(),
295
+ name: spaceName,
296
+ },
297
+ };
298
+ } catch (error) {
299
+ logger.error("❌ Failed to create space:", error);
300
+ return {
301
+ success: false,
302
+ error: error.message,
303
+ };
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Generate a new seed phrase
309
+ */
310
+ export function generateSeedPhrase() {
311
+ return generateMnemonic(english);
312
+ }
313
+
314
+ /**
315
+ * Validate seed phrase
316
+ */
317
+ export function validateSeedPhrase(seedPhrase) {
318
+ try {
319
+ // Basic validation - check if it's a valid mnemonic
320
+ const words = seedPhrase.trim().split(/\s+/);
321
+ return words.length >= 12 && words.length <= 24;
322
+ } catch (error) {
323
+ return false;
324
+ }
325
+ }
@@ -0,0 +1,79 @@
1
+ import { writable } from "svelte/store";
2
+ import { logger } from '../../lib/logger.js'
3
+
4
+ // Create a writable store for the theme
5
+ function createThemeStore() {
6
+ // Initialize theme based on system preference or localStorage
7
+ const getInitialTheme = () => {
8
+ const stored = localStorage.getItem("carbon-theme");
9
+ if (stored) return stored;
10
+
11
+ // Check system preference
12
+ if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
13
+ return "g100"; // Carbon dark theme
14
+ }
15
+
16
+ return "white"; // Carbon light theme
17
+ };
18
+
19
+ const { subscribe, set, update } = writable(getInitialTheme());
20
+
21
+ return {
22
+ subscribe,
23
+ set: (newTheme) => {
24
+ localStorage.setItem("carbon-theme", newTheme);
25
+ document.documentElement.setAttribute("theme", newTheme);
26
+ document.documentElement.style.setProperty(
27
+ "color-scheme",
28
+ newTheme === "g100" ? "dark" : "light",
29
+ );
30
+ logger.info("Theme set to:", newTheme);
31
+ set(newTheme);
32
+ },
33
+ toggle: () => {
34
+ let currentTheme;
35
+ update((theme) => {
36
+ currentTheme = theme === "white" ? "g100" : "white";
37
+ localStorage.setItem("carbon-theme", currentTheme);
38
+ document.documentElement.setAttribute("theme", currentTheme);
39
+ document.documentElement.style.setProperty(
40
+ "color-scheme",
41
+ currentTheme === "g100" ? "dark" : "light",
42
+ );
43
+ logger.info("Theme toggled to:", currentTheme);
44
+ return currentTheme;
45
+ });
46
+ },
47
+ init: () => {
48
+ const initialTheme = getInitialTheme();
49
+ logger.info("Initializing theme:", initialTheme);
50
+
51
+ // Apply theme to document
52
+ document.documentElement.setAttribute("theme", initialTheme);
53
+ document.documentElement.style.setProperty(
54
+ "color-scheme",
55
+ initialTheme === "g100" ? "dark" : "light",
56
+ );
57
+
58
+ // Update store
59
+ set(initialTheme);
60
+
61
+ // Listen for system theme changes if the user hasn't chosen a theme
62
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
63
+ mediaQuery.addEventListener("change", (e) => {
64
+ if (!localStorage.getItem("carbon-theme")) {
65
+ const newTheme = e.matches ? "g100" : "white";
66
+ logger.info("System theme changed to:", newTheme);
67
+ document.documentElement.setAttribute("theme", newTheme);
68
+ document.documentElement.style.setProperty(
69
+ "color-scheme",
70
+ newTheme === "g100" ? "dark" : "light",
71
+ );
72
+ set(newTheme);
73
+ }
74
+ });
75
+ },
76
+ };
77
+ }
78
+
79
+ export const theme = createThemeStore();
@@ -0,0 +1,161 @@
1
+ /**
2
+ * @fileoverview Making Aleph keep something: the STORE message, and nothing else.
3
+ *
4
+ * `createAlephBackend` stores without a key and says plainly that it does not
5
+ * retain. This is the other half — and it is deliberately a separate module,
6
+ * because the two have different requirements and only one of them needs a
7
+ * wallet. An application that only ever reads, or only ever uploads, should
8
+ * never have to load this file.
9
+ *
10
+ * ## No tokens move
11
+ *
12
+ * The wallet **signs a string**. Aleph then checks that the signing address has
13
+ * enough balance or credit to cover what it is being asked to keep. There is no
14
+ * transaction, no gas and no transfer: the token is the evidence, not the
15
+ * payment. Worth stating because "pay for storage with a wallet" reads as the
16
+ * opposite.
17
+ *
18
+ * ## Injected, not imported
19
+ *
20
+ * `sign` has the shape of `personal_sign` — `(address, message) => signature` —
21
+ * which is what a browser wallet provides and what a Node signer can wrap. So
22
+ * this module imports no wallet, no SDK and no key, and **no key ever passes
23
+ * through the library**. In a browser the key never leaves the wallet at all.
24
+ *
25
+ * ## The format, and where each part was established
26
+ *
27
+ * The envelope, the hash and the signing payload are taken from `relay-button`,
28
+ * which posts INSTANCE and AGGREGATE messages through the same route:
29
+ *
30
+ * item_content = JSON.stringify(content)
31
+ * item_hash = sha256hex(item_content)
32
+ * signed = sign(sender, [chain, sender, type, item_hash].join("\n"))
33
+ *
34
+ * The STORE *content* is the one part not in that codebase, and was confirmed
35
+ * against `api2.aleph.im` on 2026-09-09 — a correctly shaped message with a
36
+ * deliberately invalid signature is accepted with **202** and left pending,
37
+ * which separates "the schema is wrong" from "the signature is wrong".
38
+ *
39
+ * One trap, since it costs nothing to name: the content carries its *own*
40
+ * `item_type` and `item_hash`, and they mean something different from the
41
+ * envelope's. In the envelope they say "the message body is inline". In the
42
+ * content they say "the thing to keep is this IPFS CID".
43
+ *
44
+ * @author @NiKrause
45
+ * @requires ./aleph.js - the backend this supplies `pin` to
46
+ */
47
+
48
+ import { BackendError } from "./types.js";
49
+
50
+ export const DEFAULT_ALEPH_API_HOST = "https://api2.aleph.im";
51
+ export const DEFAULT_ALEPH_CHANNEL = "ALEPH-CLOUDSOLUTIONS";
52
+
53
+ /** Hex sha-256 of a string, via WebCrypto — present in browsers and in Node 18+. */
54
+ async function sha256Hex(payload) {
55
+ const digest = await globalThis.crypto.subtle.digest(
56
+ "SHA-256",
57
+ new TextEncoder().encode(payload),
58
+ );
59
+ return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
60
+ }
61
+
62
+ /**
63
+ * Build the unsigned STORE message for a CID.
64
+ *
65
+ * Exported so it can be tested without a wallet, and read without running one.
66
+ *
67
+ * @param {object} args
68
+ * @param {string} args.sender - the wallet address
69
+ * @param {string} args.cid - what to keep
70
+ * @param {string} [args.channel]
71
+ * @param {number} [args.now] - seconds; injected so a test is not a clock
72
+ * @param {(payload: string) => Promise<string>} [args.hasher]
73
+ */
74
+ export async function buildStoreMessage({ sender, cid, channel = DEFAULT_ALEPH_CHANNEL, now, hasher = sha256Hex }) {
75
+ const time = now ?? Date.now() / 1000;
76
+ const content = {
77
+ address: sender,
78
+ // "the thing to keep is an IPFS CID" — not the envelope's item_type
79
+ item_type: "ipfs",
80
+ item_hash: cid,
81
+ time,
82
+ };
83
+ const item_content = JSON.stringify(content);
84
+
85
+ return {
86
+ sender,
87
+ chain: "ETH",
88
+ type: "STORE",
89
+ item_hash: await hasher(item_content),
90
+ item_type: "inline",
91
+ item_content,
92
+ time,
93
+ channel,
94
+ };
95
+ }
96
+
97
+ /** What the wallet actually puts its name to. */
98
+ export const signaturePayload = (message) =>
99
+ [message.chain, message.sender, message.type, message.item_hash].join("\n");
100
+
101
+ /**
102
+ * A `pin` function for {@link createAlephBackend}.
103
+ *
104
+ * @param {object} options
105
+ * @param {string} options.sender - the wallet address doing the keeping
106
+ * @param {(address: string, message: string) => Promise<string>} options.sign -
107
+ * `personal_sign`, or anything shaped like it. The library never sees a key.
108
+ * @param {string} [options.apiHost]
109
+ * @param {string} [options.channel]
110
+ * @param {(payload: string) => Promise<string>} [options.hasher]
111
+ * @param {typeof fetch} [options.fetch]
112
+ * @param {() => number} [options.now] - seconds
113
+ * @returns {(cid: string, meta?: object) => Promise<{ itemHash: string, status: string }>}
114
+ */
115
+ export function createAlephPin(options = {}) {
116
+ const { sender, sign } = options;
117
+ const apiHost = options.apiHost || DEFAULT_ALEPH_API_HOST;
118
+ const channel = options.channel || DEFAULT_ALEPH_CHANNEL;
119
+ const hasher = options.hasher || sha256Hex;
120
+ const doFetch = options.fetch || globalThis.fetch;
121
+ const now = options.now;
122
+
123
+ if (!sender) throw new BackendError("INVALID_BACKEND", "createAlephPin needs the wallet address as `sender`");
124
+ if (typeof sign !== "function") throw new BackendError("INVALID_BACKEND", "createAlephPin needs a `sign` function");
125
+ if (typeof doFetch !== "function") throw new BackendError("INVALID_BACKEND", "createAlephPin needs fetch");
126
+
127
+ return async function pin(cid) {
128
+ const unsigned = await buildStoreMessage({ sender, cid, channel, hasher, now: now?.() });
129
+ const signature = await sign(sender, signaturePayload(unsigned));
130
+ const message = {
131
+ ...unsigned,
132
+ signature: signature.startsWith("0x") ? signature : `0x${signature}`,
133
+ };
134
+
135
+ const response = await doFetch(`${apiHost}/api/v0/messages`, {
136
+ method: "POST",
137
+ headers: { "content-type": "application/json" },
138
+ body: JSON.stringify({ message, sync: true }),
139
+ });
140
+
141
+ // 202 means Aleph took the message, not that it kept the file: the
142
+ // signature and the balance are checked afterwards. Reporting it as done
143
+ // would be the exact lie this module exists to avoid, so the caller gets
144
+ // the status it was given.
145
+ if (!response.ok && response.status !== 202) {
146
+ const detail = await response.text().catch(() => "");
147
+ throw new BackendError(
148
+ "UNSUPPORTED",
149
+ `Aleph refused the STORE message: ${response.status} ${detail.slice(0, 200)}`,
150
+ );
151
+ }
152
+
153
+ const body = await response.json().catch(() => ({}));
154
+ return {
155
+ itemHash: unsigned.item_hash,
156
+ status: body?.message_status ?? "pending",
157
+ };
158
+ };
159
+ }
160
+
161
+ export default createAlephPin;