bedrock-ts-sdk 0.0.1
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/LICENSE +21 -0
- package/README.md +539 -0
- package/dist/index.d.mts +1212 -0
- package/dist/index.d.ts +1212 -0
- package/dist/index.js +2056 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2030 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +82 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2030 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// src/client/bedrock-core.ts
|
|
9
|
+
import { AuthenticatedAlephHttpClient as AuthenticatedAlephHttpClient2 } from "@aleph-sdk/client";
|
|
10
|
+
import { getAccountFromProvider, importAccountFromPrivateKey } from "@aleph-sdk/ethereum";
|
|
11
|
+
import { PrivateKey } from "eciesjs";
|
|
12
|
+
import web3 from "web3";
|
|
13
|
+
|
|
14
|
+
// src/types/errors.ts
|
|
15
|
+
var BedrockError = class _BedrockError extends Error {
|
|
16
|
+
constructor(message, code) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.code = code;
|
|
19
|
+
this.name = "BedrockError";
|
|
20
|
+
Object.setPrototypeOf(this, _BedrockError.prototype);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
var AuthenticationError = class _AuthenticationError extends BedrockError {
|
|
24
|
+
constructor(message) {
|
|
25
|
+
super(message, "AUTH_ERROR");
|
|
26
|
+
this.name = "AuthenticationError";
|
|
27
|
+
Object.setPrototypeOf(this, _AuthenticationError.prototype);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
var EncryptionError = class _EncryptionError extends BedrockError {
|
|
31
|
+
constructor(message) {
|
|
32
|
+
super(message, "ENCRYPTION_ERROR");
|
|
33
|
+
this.name = "EncryptionError";
|
|
34
|
+
Object.setPrototypeOf(this, _EncryptionError.prototype);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
var FileError = class _FileError extends BedrockError {
|
|
38
|
+
constructor(message) {
|
|
39
|
+
super(message, "FILE_ERROR");
|
|
40
|
+
this.name = "FileError";
|
|
41
|
+
Object.setPrototypeOf(this, _FileError.prototype);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var FileNotFoundError = class _FileNotFoundError extends FileError {
|
|
45
|
+
constructor(path) {
|
|
46
|
+
super(`File not found: ${path}`);
|
|
47
|
+
this.name = "FileNotFoundError";
|
|
48
|
+
this.code = "FILE_NOT_FOUND";
|
|
49
|
+
Object.setPrototypeOf(this, _FileNotFoundError.prototype);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var ContactError = class _ContactError extends BedrockError {
|
|
53
|
+
constructor(message) {
|
|
54
|
+
super(message, "CONTACT_ERROR");
|
|
55
|
+
this.name = "ContactError";
|
|
56
|
+
Object.setPrototypeOf(this, _ContactError.prototype);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
var KnowledgeBaseError = class _KnowledgeBaseError extends BedrockError {
|
|
60
|
+
constructor(message) {
|
|
61
|
+
super(message, "KB_ERROR");
|
|
62
|
+
this.name = "KnowledgeBaseError";
|
|
63
|
+
Object.setPrototypeOf(this, _KnowledgeBaseError.prototype);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
var CreditError = class _CreditError extends BedrockError {
|
|
67
|
+
constructor(message) {
|
|
68
|
+
super(message, "CREDIT_ERROR");
|
|
69
|
+
this.name = "CreditError";
|
|
70
|
+
Object.setPrototypeOf(this, _CreditError.prototype);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var NetworkError = class _NetworkError extends BedrockError {
|
|
74
|
+
constructor(message) {
|
|
75
|
+
super(message, "NETWORK_ERROR");
|
|
76
|
+
this.name = "NetworkError";
|
|
77
|
+
Object.setPrototypeOf(this, _NetworkError.prototype);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
var ValidationError = class _ValidationError extends BedrockError {
|
|
81
|
+
constructor(message) {
|
|
82
|
+
super(message, "VALIDATION_ERROR");
|
|
83
|
+
this.name = "ValidationError";
|
|
84
|
+
Object.setPrototypeOf(this, _ValidationError.prototype);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// src/types/schemas.ts
|
|
89
|
+
import { z } from "zod";
|
|
90
|
+
var BEDROCK_MESSAGE = "Bedrock.im";
|
|
91
|
+
var SECURITY_AGGREGATE_KEY = "security";
|
|
92
|
+
var ALEPH_GENERAL_CHANNEL = "BEDROCK_STORAGE";
|
|
93
|
+
var AGGREGATE_KEYS = {
|
|
94
|
+
FILE_ENTRIES: "bedrock_file_entries",
|
|
95
|
+
CONTACTS: "bedrock_contacts",
|
|
96
|
+
KNOWLEDGE_BASES: "bedrock_knowledge_bases",
|
|
97
|
+
CREDITS: "credits"
|
|
98
|
+
};
|
|
99
|
+
var POST_TYPES = {
|
|
100
|
+
FILE: "bedrock_file",
|
|
101
|
+
PUBLIC_FILE: "bedrock_public_file"
|
|
102
|
+
};
|
|
103
|
+
var HexString64Schema = z.string().length(64).regex(/^[0-9a-f]{64}$/);
|
|
104
|
+
var HexString32Schema = z.string().length(32).regex(/^[0-9a-f]{32}$/);
|
|
105
|
+
var DatetimeSchema = z.string().datetime();
|
|
106
|
+
var AddressSchema = z.string().regex(/^0x[a-fA-F0-9]{40}$/);
|
|
107
|
+
var FileEntrySchema = z.object({
|
|
108
|
+
path: z.string(),
|
|
109
|
+
// Encrypted path
|
|
110
|
+
post_hash: HexString64Schema,
|
|
111
|
+
shared_with: z.array(z.string()).default([])
|
|
112
|
+
// Public keys of contacts
|
|
113
|
+
});
|
|
114
|
+
var FileMetaEncryptedSchema = z.object({
|
|
115
|
+
name: z.string(),
|
|
116
|
+
// Encrypted filename
|
|
117
|
+
path: z.string(),
|
|
118
|
+
// Encrypted path
|
|
119
|
+
key: HexString64Schema,
|
|
120
|
+
// Encrypted AES key
|
|
121
|
+
iv: HexString32Schema,
|
|
122
|
+
// Encrypted IV
|
|
123
|
+
store_hash: HexString64Schema,
|
|
124
|
+
// Aleph STORE hash
|
|
125
|
+
size: z.string(),
|
|
126
|
+
// Encrypted size
|
|
127
|
+
created_at: z.string(),
|
|
128
|
+
// Encrypted datetime
|
|
129
|
+
deleted_at: z.string().nullable(),
|
|
130
|
+
// Encrypted datetime or null
|
|
131
|
+
shared_keys: z.record(z.string(), z.object({
|
|
132
|
+
key: z.string(),
|
|
133
|
+
// Encrypted key for recipient
|
|
134
|
+
iv: z.string()
|
|
135
|
+
// Encrypted IV for recipient
|
|
136
|
+
})).default({})
|
|
137
|
+
});
|
|
138
|
+
var FileMetaSchema = z.object({
|
|
139
|
+
name: z.string(),
|
|
140
|
+
path: z.string(),
|
|
141
|
+
key: HexString64Schema,
|
|
142
|
+
iv: HexString32Schema,
|
|
143
|
+
store_hash: HexString64Schema,
|
|
144
|
+
size: z.number(),
|
|
145
|
+
created_at: DatetimeSchema,
|
|
146
|
+
deleted_at: DatetimeSchema.nullable(),
|
|
147
|
+
shared_keys: z.record(z.string(), z.object({
|
|
148
|
+
key: HexString64Schema,
|
|
149
|
+
iv: HexString32Schema
|
|
150
|
+
})).default({})
|
|
151
|
+
});
|
|
152
|
+
var PublicFileMetaSchema = z.object({
|
|
153
|
+
name: z.string(),
|
|
154
|
+
size: z.number(),
|
|
155
|
+
created_at: DatetimeSchema,
|
|
156
|
+
store_hash: HexString64Schema,
|
|
157
|
+
username: z.string()
|
|
158
|
+
});
|
|
159
|
+
var ContactSchema = z.object({
|
|
160
|
+
name: z.string(),
|
|
161
|
+
address: AddressSchema,
|
|
162
|
+
public_key: z.string()
|
|
163
|
+
// Hex-encoded public key
|
|
164
|
+
});
|
|
165
|
+
var ContactsAggregateSchema = z.object({
|
|
166
|
+
contacts: z.array(ContactSchema).default([])
|
|
167
|
+
});
|
|
168
|
+
var KnowledgeBaseSchema = z.object({
|
|
169
|
+
name: z.string(),
|
|
170
|
+
file_paths: z.array(z.string()).default([]),
|
|
171
|
+
// Encrypted paths
|
|
172
|
+
created_at: DatetimeSchema,
|
|
173
|
+
updated_at: DatetimeSchema
|
|
174
|
+
});
|
|
175
|
+
var KnowledgeBasesAggregateSchema = z.object({
|
|
176
|
+
knowledge_bases: z.array(KnowledgeBaseSchema).default([])
|
|
177
|
+
});
|
|
178
|
+
var CreditTransactionSchema = z.object({
|
|
179
|
+
id: z.string(),
|
|
180
|
+
amount: z.number(),
|
|
181
|
+
type: z.enum(["top_up", "deduct"]),
|
|
182
|
+
timestamp: z.number(),
|
|
183
|
+
description: z.string(),
|
|
184
|
+
txHash: z.string().optional()
|
|
185
|
+
});
|
|
186
|
+
var UserCreditSchema = z.object({
|
|
187
|
+
balance: z.number().default(0),
|
|
188
|
+
transactions: z.array(CreditTransactionSchema).default([])
|
|
189
|
+
});
|
|
190
|
+
var CreditAggregateSchema = z.record(z.string(), UserCreditSchema);
|
|
191
|
+
var FileEntriesAggregateSchema = z.object({
|
|
192
|
+
files: z.array(FileEntrySchema).default([])
|
|
193
|
+
});
|
|
194
|
+
var SecurityAggregateSchema = z.object({
|
|
195
|
+
authorizations: z.array(z.object({
|
|
196
|
+
address: AddressSchema,
|
|
197
|
+
chain: z.string(),
|
|
198
|
+
channels: z.array(z.string()).optional(),
|
|
199
|
+
post_types: z.array(z.string()).optional(),
|
|
200
|
+
aggregate_keys: z.array(z.string()).optional()
|
|
201
|
+
}))
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// src/client/aleph-service.ts
|
|
205
|
+
import { AuthenticatedAlephHttpClient } from "@aleph-sdk/client";
|
|
206
|
+
import { ItemType } from "@aleph-sdk/message";
|
|
207
|
+
import { z as z2 } from "zod";
|
|
208
|
+
var AlephService = class {
|
|
209
|
+
constructor(account, channel = ALEPH_GENERAL_CHANNEL, apiServer = "https://api2.aleph.im") {
|
|
210
|
+
this.account = account;
|
|
211
|
+
this.channel = channel;
|
|
212
|
+
this.subAccountClient = new AuthenticatedAlephHttpClient(account, apiServer);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Get the account address
|
|
216
|
+
*/
|
|
217
|
+
getAddress() {
|
|
218
|
+
return this.account.address;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Get the account's public key
|
|
222
|
+
*/
|
|
223
|
+
getPublicKey() {
|
|
224
|
+
return this.account.publicKey || "";
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Get the underlying Aleph client
|
|
228
|
+
*/
|
|
229
|
+
getClient() {
|
|
230
|
+
return this.subAccountClient;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Get the account
|
|
234
|
+
*/
|
|
235
|
+
getAccount() {
|
|
236
|
+
return this.account;
|
|
237
|
+
}
|
|
238
|
+
// ============================================================================
|
|
239
|
+
// STORE operations (file storage)
|
|
240
|
+
// ============================================================================
|
|
241
|
+
/**
|
|
242
|
+
* Upload a file to Aleph storage
|
|
243
|
+
* @param fileObject - File content as Buffer or File
|
|
244
|
+
* @returns Store message
|
|
245
|
+
*/
|
|
246
|
+
async uploadFile(fileObject) {
|
|
247
|
+
try {
|
|
248
|
+
return await this.subAccountClient.createStore({
|
|
249
|
+
fileObject,
|
|
250
|
+
storageEngine: ItemType.ipfs,
|
|
251
|
+
channel: this.channel
|
|
252
|
+
});
|
|
253
|
+
} catch (error) {
|
|
254
|
+
throw new NetworkError(`Failed to upload file: ${error.message}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Download a file from Aleph storage
|
|
259
|
+
* @param storeHash - The STORE message item_hash
|
|
260
|
+
* @returns File content as ArrayBuffer
|
|
261
|
+
*/
|
|
262
|
+
async downloadFile(storeHash) {
|
|
263
|
+
try {
|
|
264
|
+
const ipfsHash = await this.subAccountClient.getMessage(storeHash);
|
|
265
|
+
const ContentSchema = z2.object({
|
|
266
|
+
address: z2.string(),
|
|
267
|
+
item_type: z2.string(),
|
|
268
|
+
item_hash: z2.string(),
|
|
269
|
+
time: z2.number()
|
|
270
|
+
});
|
|
271
|
+
const { success, data } = ContentSchema.safeParse(ipfsHash.content);
|
|
272
|
+
if (!success) throw new Error(`Invalid data from Aleph: ${data}`);
|
|
273
|
+
return this.subAccountClient.downloadFile(data.item_hash);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
throw new NetworkError(`Failed to download file ${storeHash}: ${error.message}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Delete files from Aleph storage
|
|
280
|
+
* @param itemHashes - Array of item hashes to forget
|
|
281
|
+
* @returns Forget message
|
|
282
|
+
*/
|
|
283
|
+
async deleteFiles(itemHashes) {
|
|
284
|
+
try {
|
|
285
|
+
return this.subAccountClient.forget({ hashes: itemHashes });
|
|
286
|
+
} catch (error) {
|
|
287
|
+
throw new NetworkError(`Failed to delete files: ${error.message}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// ============================================================================
|
|
291
|
+
// AGGREGATE operations (key-value storage)
|
|
292
|
+
// ============================================================================
|
|
293
|
+
async createAggregate(key, content) {
|
|
294
|
+
try {
|
|
295
|
+
return this.subAccountClient.createAggregate({
|
|
296
|
+
key,
|
|
297
|
+
content,
|
|
298
|
+
channel: this.channel,
|
|
299
|
+
address: this.account.address
|
|
300
|
+
});
|
|
301
|
+
} catch (error) {
|
|
302
|
+
throw new NetworkError(`Failed to create aggregate: ${error.message}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
async fetchAggregate(key, schema, owner = this.account.address) {
|
|
306
|
+
try {
|
|
307
|
+
const unparsedData = await this.subAccountClient.fetchAggregate(owner, key);
|
|
308
|
+
const { success, data, error } = schema.safeParse(unparsedData);
|
|
309
|
+
if (!success)
|
|
310
|
+
throw new Error(`Invalid data from Aleph: ${error.message}, data was ${JSON.stringify(unparsedData)}`);
|
|
311
|
+
return data;
|
|
312
|
+
} catch (error) {
|
|
313
|
+
throw new NetworkError(`Failed to fetch aggregate: ${error.message}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
async updateAggregate(key, schema, update_content) {
|
|
317
|
+
try {
|
|
318
|
+
const currentContent = await this.fetchAggregate(key, schema);
|
|
319
|
+
const newContent = await update_content(currentContent);
|
|
320
|
+
return await this.createAggregate(key, newContent);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
throw new NetworkError(`Failed to update aggregate: ${error.message}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// ============================================================================
|
|
326
|
+
// POST operations (JSON messages)
|
|
327
|
+
// ============================================================================
|
|
328
|
+
async createPost(type, content) {
|
|
329
|
+
try {
|
|
330
|
+
return this.subAccountClient.createPost({
|
|
331
|
+
postType: type,
|
|
332
|
+
content,
|
|
333
|
+
channel: this.channel,
|
|
334
|
+
address: this.account.address
|
|
335
|
+
});
|
|
336
|
+
} catch (error) {
|
|
337
|
+
throw new NetworkError(`Failed to create post: ${error.message}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
async fetchPosts(type, schema, addresses = [this.account.address], hashes = []) {
|
|
341
|
+
try {
|
|
342
|
+
return z2.array(schema).parse(
|
|
343
|
+
(await this.subAccountClient.getPosts({
|
|
344
|
+
channels: [this.channel],
|
|
345
|
+
types: [type],
|
|
346
|
+
addresses,
|
|
347
|
+
hashes
|
|
348
|
+
})).posts.map((post) => post.content)
|
|
349
|
+
);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
throw new NetworkError(`Failed to fetch posts: ${error.message}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
async fetchPost(type, schema, addresses = [this.account.address], hash) {
|
|
355
|
+
try {
|
|
356
|
+
return schema.parse(
|
|
357
|
+
(await this.subAccountClient.getPost({
|
|
358
|
+
channels: [this.channel],
|
|
359
|
+
types: [type],
|
|
360
|
+
addresses,
|
|
361
|
+
hashes: [hash]
|
|
362
|
+
})).content
|
|
363
|
+
);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
throw new NetworkError(`Failed to fetch post: ${error.message}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
async updatePost(type, hash, addresses, schema, update_content) {
|
|
369
|
+
try {
|
|
370
|
+
const currentContent = await this.fetchPost(type, schema, addresses, hash);
|
|
371
|
+
const newContent = await update_content(currentContent);
|
|
372
|
+
return await this.subAccountClient.createPost({
|
|
373
|
+
postType: type,
|
|
374
|
+
content: newContent,
|
|
375
|
+
ref: hash,
|
|
376
|
+
channel: this.channel,
|
|
377
|
+
address: this.account.address
|
|
378
|
+
});
|
|
379
|
+
} catch (error) {
|
|
380
|
+
throw new NetworkError(`Failed to update post: ${error.message}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
// src/client/bedrock-core.ts
|
|
386
|
+
var BedrockCore = class _BedrockCore {
|
|
387
|
+
constructor(mainAccount, subAccount, alephService, encryptionPrivateKey, _config) {
|
|
388
|
+
this.mainAccount = mainAccount;
|
|
389
|
+
this.subAccount = subAccount;
|
|
390
|
+
this.alephService = alephService;
|
|
391
|
+
this.encryptionPrivateKey = encryptionPrivateKey;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Initialize from signature hash (matches Bedrock app pattern)
|
|
395
|
+
* @param signatureHash - Signature hash from wallet
|
|
396
|
+
* @param provider - EIP-1193 provider (for MetaMask/Rabby)
|
|
397
|
+
* @param config - Optional configuration
|
|
398
|
+
*/
|
|
399
|
+
static async fromSignature(signatureHash, provider, config) {
|
|
400
|
+
try {
|
|
401
|
+
const cfg = {
|
|
402
|
+
channel: config?.channel || ALEPH_GENERAL_CHANNEL,
|
|
403
|
+
apiServer: config?.apiServer || "https://api2.aleph.im"
|
|
404
|
+
};
|
|
405
|
+
const privateKey = web3.utils.sha3(signatureHash);
|
|
406
|
+
if (!privateKey) {
|
|
407
|
+
throw new AuthenticationError("Failed to derive private key from signature");
|
|
408
|
+
}
|
|
409
|
+
const encryptionPrivateKey = PrivateKey.fromHex(privateKey);
|
|
410
|
+
let mainAccount;
|
|
411
|
+
if (provider?.id && ["io.rabby", "io.metamask"].includes(provider.id)) {
|
|
412
|
+
if (typeof window !== "undefined" && window.ethereum) {
|
|
413
|
+
mainAccount = await getAccountFromProvider(window.ethereum);
|
|
414
|
+
} else {
|
|
415
|
+
throw new AuthenticationError("window.ethereum not available");
|
|
416
|
+
}
|
|
417
|
+
} else {
|
|
418
|
+
mainAccount = await getAccountFromProvider(provider);
|
|
419
|
+
}
|
|
420
|
+
const subAccount = importAccountFromPrivateKey(privateKey);
|
|
421
|
+
await _BedrockCore.setupSecurityPermissions(mainAccount, subAccount, cfg);
|
|
422
|
+
const alephService = new AlephService(subAccount, cfg.channel, cfg.apiServer);
|
|
423
|
+
return new _BedrockCore(mainAccount, subAccount, alephService, encryptionPrivateKey, cfg);
|
|
424
|
+
} catch (error) {
|
|
425
|
+
throw new AuthenticationError(`Failed to initialize from signature: ${error.message}`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Create BedrockCore from a private key (for testing/CLI)
|
|
430
|
+
* @param privateKey - Ethereum private key (hex string with or without 0x prefix)
|
|
431
|
+
* @param config - Optional configuration
|
|
432
|
+
*/
|
|
433
|
+
static async fromPrivateKey(privateKey, config) {
|
|
434
|
+
try {
|
|
435
|
+
const cfg = {
|
|
436
|
+
channel: config?.channel || ALEPH_GENERAL_CHANNEL,
|
|
437
|
+
apiServer: config?.apiServer || "https://api2.aleph.im"
|
|
438
|
+
};
|
|
439
|
+
const key = privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`;
|
|
440
|
+
const mainAccount = importAccountFromPrivateKey(key);
|
|
441
|
+
const signatureHash = web3.utils.sha3(key + BEDROCK_MESSAGE);
|
|
442
|
+
if (!signatureHash) {
|
|
443
|
+
throw new AuthenticationError("Failed to derive signature");
|
|
444
|
+
}
|
|
445
|
+
const subPrivateKey = web3.utils.sha3(signatureHash);
|
|
446
|
+
if (!subPrivateKey) {
|
|
447
|
+
throw new AuthenticationError("Failed to derive sub-account key");
|
|
448
|
+
}
|
|
449
|
+
const encryptionPrivateKey = PrivateKey.fromHex(subPrivateKey);
|
|
450
|
+
const subAccount = importAccountFromPrivateKey(subPrivateKey);
|
|
451
|
+
await _BedrockCore.setupSecurityPermissions(mainAccount, subAccount, cfg);
|
|
452
|
+
const alephService = new AlephService(subAccount, cfg.channel, cfg.apiServer);
|
|
453
|
+
return new _BedrockCore(mainAccount, subAccount, alephService, encryptionPrivateKey, cfg);
|
|
454
|
+
} catch (error) {
|
|
455
|
+
throw new AuthenticationError(`Failed to import account: ${error.message}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Create BedrockCore from a wallet provider (e.g., MetaMask)
|
|
460
|
+
* This will automatically request signature from the user
|
|
461
|
+
* @param provider - EIP-1193 provider
|
|
462
|
+
* @param config - Optional configuration
|
|
463
|
+
*/
|
|
464
|
+
static async fromProvider(provider, config) {
|
|
465
|
+
try {
|
|
466
|
+
const accounts = await provider.request({ method: "eth_requestAccounts" });
|
|
467
|
+
if (!accounts || accounts.length === 0) {
|
|
468
|
+
throw new AuthenticationError("No accounts found");
|
|
469
|
+
}
|
|
470
|
+
const signature = await provider.request({
|
|
471
|
+
method: "personal_sign",
|
|
472
|
+
params: [BEDROCK_MESSAGE, accounts[0]]
|
|
473
|
+
});
|
|
474
|
+
return await _BedrockCore.fromSignature(signature, provider, config);
|
|
475
|
+
} catch (error) {
|
|
476
|
+
throw new AuthenticationError(`Failed to connect to provider: ${error.message}`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Get the main account
|
|
481
|
+
*/
|
|
482
|
+
getMainAccount() {
|
|
483
|
+
return this.mainAccount;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Get the sub-account
|
|
487
|
+
*/
|
|
488
|
+
getSubAccount() {
|
|
489
|
+
return this.subAccount;
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Get the AlephService instance
|
|
493
|
+
*/
|
|
494
|
+
getAlephService() {
|
|
495
|
+
return this.alephService;
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Get the encryption key
|
|
499
|
+
*/
|
|
500
|
+
getEncryptionKey() {
|
|
501
|
+
return Buffer.from(this.encryptionPrivateKey.secret);
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Get the encryption private key
|
|
505
|
+
*/
|
|
506
|
+
getEncryptionPrivateKey() {
|
|
507
|
+
return this.encryptionPrivateKey;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Get the main account's address
|
|
511
|
+
*/
|
|
512
|
+
getMainAddress() {
|
|
513
|
+
return this.mainAccount.address;
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Get the sub-account's address
|
|
517
|
+
*/
|
|
518
|
+
getSubAddress() {
|
|
519
|
+
return this.subAccount.address;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Get the main account's public key
|
|
523
|
+
*/
|
|
524
|
+
getPublicKey() {
|
|
525
|
+
return this.mainAccount.publicKey || "";
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Get the sub-account's private key (as hex string)
|
|
529
|
+
*/
|
|
530
|
+
getSubAccountPrivateKey() {
|
|
531
|
+
return this.encryptionPrivateKey.toHex();
|
|
532
|
+
}
|
|
533
|
+
// ============================================================================
|
|
534
|
+
// Static helper methods
|
|
535
|
+
// ============================================================================
|
|
536
|
+
/**
|
|
537
|
+
* Setup security permissions (matches Bedrock app pattern)
|
|
538
|
+
*/
|
|
539
|
+
static async setupSecurityPermissions(mainAccount, subAccount, config) {
|
|
540
|
+
try {
|
|
541
|
+
const accountClient = new AuthenticatedAlephHttpClient2(mainAccount, config.apiServer);
|
|
542
|
+
try {
|
|
543
|
+
const securitySettings = await accountClient.fetchAggregate(
|
|
544
|
+
mainAccount.address,
|
|
545
|
+
SECURITY_AGGREGATE_KEY
|
|
546
|
+
);
|
|
547
|
+
const authorizations = securitySettings?.authorizations || [];
|
|
548
|
+
const isAuthorized = authorizations.find(
|
|
549
|
+
(auth) => auth.address === subAccount.address && auth.types === void 0 && auth.channels?.includes(config.channel)
|
|
550
|
+
);
|
|
551
|
+
if (!isAuthorized) {
|
|
552
|
+
const oldAuthorizations = authorizations.filter((a) => a.address !== subAccount.address);
|
|
553
|
+
await accountClient.createAggregate({
|
|
554
|
+
key: SECURITY_AGGREGATE_KEY,
|
|
555
|
+
content: {
|
|
556
|
+
authorizations: [
|
|
557
|
+
...oldAuthorizations,
|
|
558
|
+
{
|
|
559
|
+
address: subAccount.address,
|
|
560
|
+
channels: [config.channel]
|
|
561
|
+
}
|
|
562
|
+
]
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
} catch (_error) {
|
|
567
|
+
await accountClient.createAggregate({
|
|
568
|
+
key: SECURITY_AGGREGATE_KEY,
|
|
569
|
+
content: {
|
|
570
|
+
authorizations: [
|
|
571
|
+
{
|
|
572
|
+
address: subAccount.address,
|
|
573
|
+
channels: [config.channel]
|
|
574
|
+
}
|
|
575
|
+
]
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
} catch (error) {
|
|
580
|
+
throw new AuthenticationError(`Failed to setup security permissions: ${error.message}`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
// src/crypto/encryption.ts
|
|
586
|
+
import { encrypt as eciesEncrypt, decrypt as eciesDecrypt } from "eciesjs";
|
|
587
|
+
var CryptoUtils = class {
|
|
588
|
+
/**
|
|
589
|
+
* Get crypto implementation (Node.js or browser)
|
|
590
|
+
*/
|
|
591
|
+
static getCrypto() {
|
|
592
|
+
if (this.isBrowser) {
|
|
593
|
+
return window.crypto;
|
|
594
|
+
}
|
|
595
|
+
const nodeCrypto = __require("crypto");
|
|
596
|
+
return nodeCrypto.webcrypto || nodeCrypto;
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Generate random bytes
|
|
600
|
+
*/
|
|
601
|
+
static getRandomBytes(length) {
|
|
602
|
+
const crypto = this.getCrypto();
|
|
603
|
+
const bytes = new Uint8Array(length);
|
|
604
|
+
if (this.isBrowser) {
|
|
605
|
+
crypto.getRandomValues(bytes);
|
|
606
|
+
} else {
|
|
607
|
+
const nodeCrypto = __require("crypto");
|
|
608
|
+
const randomBytes = nodeCrypto.randomBytes(length);
|
|
609
|
+
bytes.set(randomBytes);
|
|
610
|
+
}
|
|
611
|
+
return bytes;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Convert buffer to hex string
|
|
615
|
+
*/
|
|
616
|
+
static bufferToHex(buffer) {
|
|
617
|
+
return Buffer.from(buffer).toString("hex");
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Convert hex string to buffer
|
|
621
|
+
*/
|
|
622
|
+
static hexToBuffer(hex) {
|
|
623
|
+
return Buffer.from(hex, "hex");
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Hash using SHA-256
|
|
627
|
+
*/
|
|
628
|
+
static async sha256(data) {
|
|
629
|
+
const bytes = typeof data === "string" ? Buffer.from(data, "utf-8") : data;
|
|
630
|
+
if (this.isBrowser) {
|
|
631
|
+
const crypto = this.getCrypto();
|
|
632
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", bytes);
|
|
633
|
+
return Buffer.from(hashBuffer);
|
|
634
|
+
} else {
|
|
635
|
+
const nodeCrypto = __require("crypto");
|
|
636
|
+
return nodeCrypto.createHash("sha256").update(bytes).digest();
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
CryptoUtils.isBrowser = typeof window !== "undefined" && typeof window.crypto !== "undefined";
|
|
641
|
+
var EncryptionService = class {
|
|
642
|
+
/**
|
|
643
|
+
* Generate a random encryption key (32 bytes for AES-256)
|
|
644
|
+
*/
|
|
645
|
+
static generateKey() {
|
|
646
|
+
return Buffer.from(CryptoUtils.getRandomBytes(32));
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Generate a random initialization vector (16 bytes for AES)
|
|
650
|
+
*/
|
|
651
|
+
static generateIv() {
|
|
652
|
+
return Buffer.from(CryptoUtils.getRandomBytes(16));
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Encrypt data using AES-256-CBC
|
|
656
|
+
* @param data - Data to encrypt
|
|
657
|
+
* @param key - 32-byte encryption key
|
|
658
|
+
* @param iv - 16-byte initialization vector
|
|
659
|
+
* @returns Hex-encoded encrypted data
|
|
660
|
+
*/
|
|
661
|
+
static async encrypt(data, key, iv) {
|
|
662
|
+
try {
|
|
663
|
+
if (key.length !== 32) {
|
|
664
|
+
throw new EncryptionError("Key must be 32 bytes for AES-256");
|
|
665
|
+
}
|
|
666
|
+
if (iv.length !== 16) {
|
|
667
|
+
throw new EncryptionError("IV must be 16 bytes");
|
|
668
|
+
}
|
|
669
|
+
if (CryptoUtils.isBrowser) {
|
|
670
|
+
return await this.encryptBrowser(data, key, iv);
|
|
671
|
+
} else {
|
|
672
|
+
return this.encryptNode(data, key, iv);
|
|
673
|
+
}
|
|
674
|
+
} catch (error) {
|
|
675
|
+
throw new EncryptionError(`Encryption failed: ${error.message}`);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Decrypt data using AES-256-CBC
|
|
680
|
+
* @param encryptedData - Hex-encoded encrypted data
|
|
681
|
+
* @param key - 32-byte encryption key
|
|
682
|
+
* @param iv - 16-byte initialization vector
|
|
683
|
+
* @returns Decrypted string
|
|
684
|
+
*/
|
|
685
|
+
static async decrypt(encryptedData, key, iv) {
|
|
686
|
+
try {
|
|
687
|
+
if (key.length !== 32) {
|
|
688
|
+
throw new EncryptionError("Key must be 32 bytes for AES-256");
|
|
689
|
+
}
|
|
690
|
+
if (iv.length !== 16) {
|
|
691
|
+
throw new EncryptionError("IV must be 16 bytes");
|
|
692
|
+
}
|
|
693
|
+
if (CryptoUtils.isBrowser) {
|
|
694
|
+
return await this.decryptBrowser(encryptedData, key, iv);
|
|
695
|
+
} else {
|
|
696
|
+
return this.decryptNode(encryptedData, key, iv);
|
|
697
|
+
}
|
|
698
|
+
} catch (error) {
|
|
699
|
+
throw new EncryptionError(`Decryption failed: ${error.message}`);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Encrypt file buffer using AES-256-CBC
|
|
704
|
+
* @param fileBuffer - File data as Buffer or ArrayBuffer
|
|
705
|
+
* @param key - 32-byte encryption key
|
|
706
|
+
* @param iv - 16-byte initialization vector
|
|
707
|
+
* @returns Encrypted file buffer
|
|
708
|
+
*/
|
|
709
|
+
static async encryptFile(fileBuffer, key, iv) {
|
|
710
|
+
try {
|
|
711
|
+
const buffer = Buffer.isBuffer(fileBuffer) ? fileBuffer : Buffer.from(fileBuffer);
|
|
712
|
+
if (CryptoUtils.isBrowser) {
|
|
713
|
+
return await this.encryptFileBrowser(buffer, key, iv);
|
|
714
|
+
} else {
|
|
715
|
+
return this.encryptFileNode(buffer, key, iv);
|
|
716
|
+
}
|
|
717
|
+
} catch (error) {
|
|
718
|
+
throw new EncryptionError(`File encryption failed: ${error.message}`);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Decrypt file buffer using AES-256-CBC
|
|
723
|
+
* @param encryptedBuffer - Encrypted file data
|
|
724
|
+
* @param key - 32-byte encryption key
|
|
725
|
+
* @param iv - 16-byte initialization vector
|
|
726
|
+
* @returns Decrypted file buffer
|
|
727
|
+
*/
|
|
728
|
+
static async decryptFile(encryptedBuffer, key, iv) {
|
|
729
|
+
try {
|
|
730
|
+
const buffer = Buffer.isBuffer(encryptedBuffer) ? encryptedBuffer : Buffer.from(encryptedBuffer);
|
|
731
|
+
if (CryptoUtils.isBrowser) {
|
|
732
|
+
return await this.decryptFileBrowser(buffer, key, iv);
|
|
733
|
+
} else {
|
|
734
|
+
return this.decryptFileNode(buffer, key, iv);
|
|
735
|
+
}
|
|
736
|
+
} catch (error) {
|
|
737
|
+
throw new EncryptionError(`File decryption failed: ${error.message}`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Encrypt data using ECIES (Elliptic Curve Integrated Encryption Scheme)
|
|
742
|
+
* @param data - Data to encrypt
|
|
743
|
+
* @param publicKey - Recipient's public key (hex or Buffer)
|
|
744
|
+
* @returns Hex-encoded encrypted data
|
|
745
|
+
*/
|
|
746
|
+
static encryptEcies(data, publicKey) {
|
|
747
|
+
try {
|
|
748
|
+
const pubKeyBuffer = typeof publicKey === "string" ? CryptoUtils.hexToBuffer(publicKey) : publicKey;
|
|
749
|
+
const dataBuffer = Buffer.from(data, "utf-8");
|
|
750
|
+
const encrypted = eciesEncrypt(pubKeyBuffer, dataBuffer);
|
|
751
|
+
return encrypted.toString("hex");
|
|
752
|
+
} catch (error) {
|
|
753
|
+
throw new EncryptionError(`ECIES encryption failed: ${error.message}`);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Decrypt data using ECIES
|
|
758
|
+
* @param encryptedData - Hex-encoded encrypted data
|
|
759
|
+
* @param privateKey - Recipient's private key (hex or Buffer)
|
|
760
|
+
* @returns Decrypted string
|
|
761
|
+
*/
|
|
762
|
+
static decryptEcies(encryptedData, privateKey) {
|
|
763
|
+
try {
|
|
764
|
+
const privKeyBuffer = typeof privateKey === "string" ? CryptoUtils.hexToBuffer(privateKey) : privateKey;
|
|
765
|
+
const encryptedBuffer = CryptoUtils.hexToBuffer(encryptedData);
|
|
766
|
+
const decrypted = eciesDecrypt(privKeyBuffer, encryptedBuffer);
|
|
767
|
+
return decrypted.toString("utf-8");
|
|
768
|
+
} catch (error) {
|
|
769
|
+
throw new EncryptionError(`ECIES decryption failed: ${error.message}`);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Hash data using SHA-256
|
|
774
|
+
*/
|
|
775
|
+
static async hash(data) {
|
|
776
|
+
const hashBuffer = await CryptoUtils.sha256(data);
|
|
777
|
+
return CryptoUtils.bufferToHex(hashBuffer);
|
|
778
|
+
}
|
|
779
|
+
// ============================================================================
|
|
780
|
+
// Node.js implementations
|
|
781
|
+
// ============================================================================
|
|
782
|
+
static encryptNode(data, key, iv) {
|
|
783
|
+
const crypto = __require("crypto");
|
|
784
|
+
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
|
|
785
|
+
let encrypted = cipher.update(data, "utf-8", "hex");
|
|
786
|
+
encrypted += cipher.final("hex");
|
|
787
|
+
return encrypted;
|
|
788
|
+
}
|
|
789
|
+
static decryptNode(encryptedData, key, iv) {
|
|
790
|
+
const crypto = __require("crypto");
|
|
791
|
+
const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv);
|
|
792
|
+
let decrypted = decipher.update(encryptedData, "hex", "utf-8");
|
|
793
|
+
decrypted += decipher.final("utf-8");
|
|
794
|
+
return decrypted;
|
|
795
|
+
}
|
|
796
|
+
static encryptFileNode(buffer, key, iv) {
|
|
797
|
+
const crypto = __require("crypto");
|
|
798
|
+
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
|
|
799
|
+
return Buffer.concat([cipher.update(buffer), cipher.final()]);
|
|
800
|
+
}
|
|
801
|
+
static decryptFileNode(buffer, key, iv) {
|
|
802
|
+
const crypto = __require("crypto");
|
|
803
|
+
const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv);
|
|
804
|
+
return Buffer.concat([decipher.update(buffer), decipher.final()]);
|
|
805
|
+
}
|
|
806
|
+
// ============================================================================
|
|
807
|
+
// Browser implementations
|
|
808
|
+
// ============================================================================
|
|
809
|
+
static async encryptBrowser(data, key, iv) {
|
|
810
|
+
const crypto = CryptoUtils.getCrypto();
|
|
811
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
812
|
+
"raw",
|
|
813
|
+
key,
|
|
814
|
+
{ name: "AES-CBC" },
|
|
815
|
+
false,
|
|
816
|
+
["encrypt"]
|
|
817
|
+
);
|
|
818
|
+
const dataBuffer = Buffer.from(data, "utf-8");
|
|
819
|
+
const encrypted = await crypto.subtle.encrypt(
|
|
820
|
+
{ name: "AES-CBC", iv },
|
|
821
|
+
cryptoKey,
|
|
822
|
+
dataBuffer
|
|
823
|
+
);
|
|
824
|
+
return CryptoUtils.bufferToHex(Buffer.from(encrypted));
|
|
825
|
+
}
|
|
826
|
+
static async decryptBrowser(encryptedData, key, iv) {
|
|
827
|
+
const crypto = CryptoUtils.getCrypto();
|
|
828
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
829
|
+
"raw",
|
|
830
|
+
key,
|
|
831
|
+
{ name: "AES-CBC" },
|
|
832
|
+
false,
|
|
833
|
+
["decrypt"]
|
|
834
|
+
);
|
|
835
|
+
const encryptedBuffer = CryptoUtils.hexToBuffer(encryptedData);
|
|
836
|
+
const decrypted = await crypto.subtle.decrypt(
|
|
837
|
+
{ name: "AES-CBC", iv },
|
|
838
|
+
cryptoKey,
|
|
839
|
+
encryptedBuffer
|
|
840
|
+
);
|
|
841
|
+
return Buffer.from(decrypted).toString("utf-8");
|
|
842
|
+
}
|
|
843
|
+
static async encryptFileBrowser(buffer, key, iv) {
|
|
844
|
+
const crypto = CryptoUtils.getCrypto();
|
|
845
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
846
|
+
"raw",
|
|
847
|
+
key,
|
|
848
|
+
{ name: "AES-CBC" },
|
|
849
|
+
false,
|
|
850
|
+
["encrypt"]
|
|
851
|
+
);
|
|
852
|
+
const encrypted = await crypto.subtle.encrypt(
|
|
853
|
+
{ name: "AES-CBC", iv },
|
|
854
|
+
cryptoKey,
|
|
855
|
+
buffer
|
|
856
|
+
);
|
|
857
|
+
return Buffer.from(encrypted);
|
|
858
|
+
}
|
|
859
|
+
static async decryptFileBrowser(buffer, key, iv) {
|
|
860
|
+
const crypto = CryptoUtils.getCrypto();
|
|
861
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
862
|
+
"raw",
|
|
863
|
+
key,
|
|
864
|
+
{ name: "AES-CBC" },
|
|
865
|
+
false,
|
|
866
|
+
["decrypt"]
|
|
867
|
+
);
|
|
868
|
+
const decrypted = await crypto.subtle.decrypt(
|
|
869
|
+
{ name: "AES-CBC", iv },
|
|
870
|
+
cryptoKey,
|
|
871
|
+
buffer
|
|
872
|
+
);
|
|
873
|
+
return Buffer.from(decrypted);
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
// src/services/file-service.ts
|
|
878
|
+
import { AlephHttpClient } from "@aleph-sdk/client";
|
|
879
|
+
var FileService = class {
|
|
880
|
+
constructor(core) {
|
|
881
|
+
this.core = core;
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Initialize file entries aggregate if it doesn't exist
|
|
885
|
+
*/
|
|
886
|
+
async setup() {
|
|
887
|
+
const aleph = this.core.getAlephService();
|
|
888
|
+
try {
|
|
889
|
+
await aleph.fetchAggregate(
|
|
890
|
+
AGGREGATE_KEYS.FILE_ENTRIES,
|
|
891
|
+
FileEntriesAggregateSchema
|
|
892
|
+
);
|
|
893
|
+
} catch {
|
|
894
|
+
await aleph.createAggregate(AGGREGATE_KEYS.FILE_ENTRIES, { files: [] });
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Upload files with encryption
|
|
899
|
+
* @param files - Array of files to upload
|
|
900
|
+
* @param directoryPath - Optional directory path prefix
|
|
901
|
+
* @returns Array of uploaded file info
|
|
902
|
+
*/
|
|
903
|
+
async uploadFiles(files, directoryPath = "") {
|
|
904
|
+
const aleph = this.core.getAlephService();
|
|
905
|
+
const publicKey = this.core.getPublicKey();
|
|
906
|
+
const uploadedFiles = [];
|
|
907
|
+
try {
|
|
908
|
+
for (const file of files) {
|
|
909
|
+
const key = EncryptionService.generateKey();
|
|
910
|
+
const iv = EncryptionService.generateIv();
|
|
911
|
+
let fileBuffer;
|
|
912
|
+
if (file.content instanceof Buffer) {
|
|
913
|
+
fileBuffer = file.content;
|
|
914
|
+
} else {
|
|
915
|
+
const arrayBuffer = await file.content.arrayBuffer();
|
|
916
|
+
fileBuffer = Buffer.from(arrayBuffer);
|
|
917
|
+
}
|
|
918
|
+
const encryptedContent = await EncryptionService.encryptFile(fileBuffer, key, iv);
|
|
919
|
+
const storeResult = await aleph.uploadFile(encryptedContent);
|
|
920
|
+
const fullPath = directoryPath ? `${directoryPath}/${file.path}` : file.path;
|
|
921
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
922
|
+
const fileMeta = {
|
|
923
|
+
name: file.name,
|
|
924
|
+
path: fullPath,
|
|
925
|
+
key: key.toString("hex"),
|
|
926
|
+
iv: iv.toString("hex"),
|
|
927
|
+
store_hash: storeResult.item_hash,
|
|
928
|
+
size: fileBuffer.length,
|
|
929
|
+
created_at: createdAt,
|
|
930
|
+
deleted_at: null,
|
|
931
|
+
shared_keys: {}
|
|
932
|
+
};
|
|
933
|
+
const encryptedMeta = await this.encryptFileMeta(fileMeta);
|
|
934
|
+
const postResult = await aleph.createPost(POST_TYPES.FILE, encryptedMeta);
|
|
935
|
+
const encryptedPath = EncryptionService.encryptEcies(fullPath, publicKey);
|
|
936
|
+
const fileEntry = {
|
|
937
|
+
path: encryptedPath,
|
|
938
|
+
post_hash: postResult.item_hash,
|
|
939
|
+
shared_with: []
|
|
940
|
+
};
|
|
941
|
+
uploadedFiles.push({ ...fileEntry, ...fileMeta });
|
|
942
|
+
}
|
|
943
|
+
await this.saveFileEntries(uploadedFiles);
|
|
944
|
+
return uploadedFiles;
|
|
945
|
+
} catch (error) {
|
|
946
|
+
throw new FileError(`Failed to upload files: ${error.message}`);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* Download and decrypt a file
|
|
951
|
+
* @param fileInfo - File information
|
|
952
|
+
* @returns Decrypted file buffer
|
|
953
|
+
*/
|
|
954
|
+
async downloadFile(fileInfo) {
|
|
955
|
+
const aleph = this.core.getAlephService();
|
|
956
|
+
try {
|
|
957
|
+
const key = Buffer.from(fileInfo.key, "hex");
|
|
958
|
+
const iv = Buffer.from(fileInfo.iv, "hex");
|
|
959
|
+
const encryptedContent = await aleph.downloadFile(fileInfo.store_hash);
|
|
960
|
+
const decryptedContent = await EncryptionService.decryptFile(encryptedContent, key, iv);
|
|
961
|
+
return decryptedContent;
|
|
962
|
+
} catch (error) {
|
|
963
|
+
throw new FileError(`Failed to download file: ${error.message}`);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Fetch all file entries
|
|
968
|
+
*/
|
|
969
|
+
async fetchFileEntries() {
|
|
970
|
+
const aleph = this.core.getAlephService();
|
|
971
|
+
try {
|
|
972
|
+
const aggregate = await aleph.fetchAggregate(
|
|
973
|
+
AGGREGATE_KEYS.FILE_ENTRIES,
|
|
974
|
+
FileEntriesAggregateSchema
|
|
975
|
+
);
|
|
976
|
+
return aggregate.files;
|
|
977
|
+
} catch (error) {
|
|
978
|
+
throw new FileError(`Failed to fetch file entries: ${error.message}`);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Fetch file metadata from entries
|
|
983
|
+
* @param entries - File entries
|
|
984
|
+
* @param owner - Optional owner address
|
|
985
|
+
* @returns Array of full file info
|
|
986
|
+
*/
|
|
987
|
+
async fetchFilesMetaFromEntries(entries, owner) {
|
|
988
|
+
const aleph = this.core.getAlephService();
|
|
989
|
+
const privateKey = owner ? void 0 : this.core.getSubAccountPrivateKey();
|
|
990
|
+
const files = [];
|
|
991
|
+
try {
|
|
992
|
+
for (const entry of entries) {
|
|
993
|
+
try {
|
|
994
|
+
const encryptedMeta = await aleph.fetchPost(
|
|
995
|
+
POST_TYPES.FILE,
|
|
996
|
+
FileMetaEncryptedSchema,
|
|
997
|
+
owner ? [owner] : void 0,
|
|
998
|
+
entry.post_hash
|
|
999
|
+
);
|
|
1000
|
+
const decryptedMeta = await this.decryptFileMeta(encryptedMeta, privateKey);
|
|
1001
|
+
files.push({
|
|
1002
|
+
...entry,
|
|
1003
|
+
...decryptedMeta
|
|
1004
|
+
});
|
|
1005
|
+
} catch (error) {
|
|
1006
|
+
console.warn(`Failed to fetch metadata for ${entry.post_hash}:`, error);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
return files;
|
|
1010
|
+
} catch (error) {
|
|
1011
|
+
throw new FileError(`Failed to fetch files metadata: ${error.message}`);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* List all files
|
|
1016
|
+
* @param includeDeleted - Include soft-deleted files
|
|
1017
|
+
*/
|
|
1018
|
+
async listFiles(includeDeleted = false) {
|
|
1019
|
+
const entries = await this.fetchFileEntries();
|
|
1020
|
+
const files = await this.fetchFilesMetaFromEntries(entries);
|
|
1021
|
+
if (!includeDeleted) {
|
|
1022
|
+
return files.filter((f) => !f.deleted_at);
|
|
1023
|
+
}
|
|
1024
|
+
return files;
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Get a file by path
|
|
1028
|
+
* @param path - File path
|
|
1029
|
+
*/
|
|
1030
|
+
async getFile(path) {
|
|
1031
|
+
const files = await this.listFiles(true);
|
|
1032
|
+
const file = files.find((f) => f.path === path);
|
|
1033
|
+
if (!file) {
|
|
1034
|
+
throw new FileNotFoundError(path);
|
|
1035
|
+
}
|
|
1036
|
+
return file;
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Soft delete files
|
|
1040
|
+
* @param filePaths - Paths of files to delete
|
|
1041
|
+
* @param deletionDate - Optional deletion date
|
|
1042
|
+
*/
|
|
1043
|
+
async softDeleteFiles(filePaths, deletionDate) {
|
|
1044
|
+
const aleph = this.core.getAlephService();
|
|
1045
|
+
const deletedAt = (deletionDate || /* @__PURE__ */ new Date()).toISOString();
|
|
1046
|
+
try {
|
|
1047
|
+
for (const path of filePaths) {
|
|
1048
|
+
const file = await this.getFile(path);
|
|
1049
|
+
await aleph.updatePost(
|
|
1050
|
+
POST_TYPES.FILE,
|
|
1051
|
+
file.post_hash,
|
|
1052
|
+
[this.core.getMainAddress()],
|
|
1053
|
+
FileMetaEncryptedSchema,
|
|
1054
|
+
async (encryptedMeta) => {
|
|
1055
|
+
const decryptedMeta = await this.decryptFileMeta(encryptedMeta);
|
|
1056
|
+
decryptedMeta.deleted_at = deletedAt;
|
|
1057
|
+
return await this.encryptFileMeta(decryptedMeta);
|
|
1058
|
+
}
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
} catch (error) {
|
|
1062
|
+
throw new FileError(`Failed to soft delete files: ${error.message}`);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Restore soft-deleted files
|
|
1067
|
+
* @param filePaths - Paths of files to restore
|
|
1068
|
+
*/
|
|
1069
|
+
async restoreFiles(filePaths) {
|
|
1070
|
+
const aleph = this.core.getAlephService();
|
|
1071
|
+
try {
|
|
1072
|
+
for (const path of filePaths) {
|
|
1073
|
+
const file = await this.getFile(path);
|
|
1074
|
+
await aleph.updatePost(
|
|
1075
|
+
POST_TYPES.FILE,
|
|
1076
|
+
file.post_hash,
|
|
1077
|
+
[this.core.getMainAddress()],
|
|
1078
|
+
FileMetaEncryptedSchema,
|
|
1079
|
+
async (encryptedMeta) => {
|
|
1080
|
+
const decryptedMeta = await this.decryptFileMeta(encryptedMeta);
|
|
1081
|
+
decryptedMeta.deleted_at = null;
|
|
1082
|
+
return await this.encryptFileMeta(decryptedMeta);
|
|
1083
|
+
}
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
} catch (error) {
|
|
1087
|
+
throw new FileError(`Failed to restore files: ${error.message}`);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Hard delete files (permanently remove from Aleph)
|
|
1092
|
+
* @param filePaths - Paths of files to delete
|
|
1093
|
+
*/
|
|
1094
|
+
async hardDeleteFiles(filePaths) {
|
|
1095
|
+
const aleph = this.core.getAlephService();
|
|
1096
|
+
try {
|
|
1097
|
+
const files = await Promise.all(filePaths.map((path) => this.getFile(path)));
|
|
1098
|
+
await aleph.updateAggregate(
|
|
1099
|
+
AGGREGATE_KEYS.FILE_ENTRIES,
|
|
1100
|
+
FileEntriesAggregateSchema,
|
|
1101
|
+
async (aggregate) => ({
|
|
1102
|
+
files: aggregate.files.filter(
|
|
1103
|
+
(entry) => !files.some((f) => f.post_hash === entry.post_hash)
|
|
1104
|
+
)
|
|
1105
|
+
})
|
|
1106
|
+
);
|
|
1107
|
+
const hashesToForget = files.flatMap((f) => [f.store_hash, f.post_hash]);
|
|
1108
|
+
await aleph.deleteFiles(hashesToForget);
|
|
1109
|
+
} catch (error) {
|
|
1110
|
+
throw new FileError(`Failed to hard delete files: ${error.message}`);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Move/rename files
|
|
1115
|
+
* @param moves - Array of {oldPath, newPath} objects
|
|
1116
|
+
*/
|
|
1117
|
+
async moveFiles(moves) {
|
|
1118
|
+
const aleph = this.core.getAlephService();
|
|
1119
|
+
const publicKey = this.core.getPublicKey();
|
|
1120
|
+
try {
|
|
1121
|
+
for (const { oldPath, newPath } of moves) {
|
|
1122
|
+
const file = await this.getFile(oldPath);
|
|
1123
|
+
await aleph.updatePost(
|
|
1124
|
+
POST_TYPES.FILE,
|
|
1125
|
+
file.post_hash,
|
|
1126
|
+
[this.core.getMainAddress()],
|
|
1127
|
+
FileMetaEncryptedSchema,
|
|
1128
|
+
async (encryptedMeta) => {
|
|
1129
|
+
const decryptedMeta = await this.decryptFileMeta(encryptedMeta);
|
|
1130
|
+
decryptedMeta.path = newPath;
|
|
1131
|
+
decryptedMeta.name = newPath.split("/").pop() || newPath;
|
|
1132
|
+
return await this.encryptFileMeta(decryptedMeta);
|
|
1133
|
+
}
|
|
1134
|
+
);
|
|
1135
|
+
const newEncryptedPath = EncryptionService.encryptEcies(newPath, publicKey);
|
|
1136
|
+
await aleph.updateAggregate(
|
|
1137
|
+
AGGREGATE_KEYS.FILE_ENTRIES,
|
|
1138
|
+
FileEntriesAggregateSchema,
|
|
1139
|
+
async (aggregate) => ({
|
|
1140
|
+
files: aggregate.files.map(
|
|
1141
|
+
(entry) => entry.post_hash === file.post_hash ? { ...entry, path: newEncryptedPath } : entry
|
|
1142
|
+
)
|
|
1143
|
+
})
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
} catch (error) {
|
|
1147
|
+
throw new FileError(`Failed to move files: ${error.message}`);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Duplicate a file
|
|
1152
|
+
* @param sourcePath - Source file path
|
|
1153
|
+
* @param newPath - New file path
|
|
1154
|
+
*/
|
|
1155
|
+
async duplicateFile(sourcePath, newPath) {
|
|
1156
|
+
try {
|
|
1157
|
+
const sourceFile = await this.getFile(sourcePath);
|
|
1158
|
+
const content = await this.downloadFile(sourceFile);
|
|
1159
|
+
const [newFile] = await this.uploadFiles([{
|
|
1160
|
+
name: newPath.split("/").pop() || newPath,
|
|
1161
|
+
path: newPath,
|
|
1162
|
+
content
|
|
1163
|
+
}]);
|
|
1164
|
+
return newFile;
|
|
1165
|
+
} catch (error) {
|
|
1166
|
+
throw new FileError(`Failed to duplicate file: ${error.message}`);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Share a file with a contact
|
|
1171
|
+
* @param filePath - File path
|
|
1172
|
+
* @param contactPublicKey - Contact's public key
|
|
1173
|
+
*/
|
|
1174
|
+
async shareFile(filePath, contactPublicKey) {
|
|
1175
|
+
const aleph = this.core.getAlephService();
|
|
1176
|
+
try {
|
|
1177
|
+
const file = await this.getFile(filePath);
|
|
1178
|
+
const encryptedKey = EncryptionService.encryptEcies(file.key, contactPublicKey);
|
|
1179
|
+
const encryptedIv = EncryptionService.encryptEcies(file.iv, contactPublicKey);
|
|
1180
|
+
await aleph.updatePost(
|
|
1181
|
+
POST_TYPES.FILE,
|
|
1182
|
+
file.post_hash,
|
|
1183
|
+
[this.core.getMainAddress()],
|
|
1184
|
+
FileMetaEncryptedSchema,
|
|
1185
|
+
async (encryptedMeta) => {
|
|
1186
|
+
const decryptedMeta = await this.decryptFileMeta(encryptedMeta);
|
|
1187
|
+
decryptedMeta.shared_keys[contactPublicKey] = {
|
|
1188
|
+
key: encryptedKey,
|
|
1189
|
+
iv: encryptedIv
|
|
1190
|
+
};
|
|
1191
|
+
return await this.encryptFileMeta(decryptedMeta);
|
|
1192
|
+
}
|
|
1193
|
+
);
|
|
1194
|
+
await aleph.updateAggregate(
|
|
1195
|
+
AGGREGATE_KEYS.FILE_ENTRIES,
|
|
1196
|
+
FileEntriesAggregateSchema,
|
|
1197
|
+
async (aggregate) => ({
|
|
1198
|
+
files: aggregate.files.map(
|
|
1199
|
+
(entry) => entry.post_hash === file.post_hash ? { ...entry, shared_with: [.../* @__PURE__ */ new Set([...entry.shared_with, contactPublicKey])] } : entry
|
|
1200
|
+
)
|
|
1201
|
+
})
|
|
1202
|
+
);
|
|
1203
|
+
} catch (error) {
|
|
1204
|
+
throw new FileError(`Failed to share file: ${error.message}`);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Unshare a file with a contact
|
|
1209
|
+
* @param filePath - File path
|
|
1210
|
+
* @param contactPublicKey - Contact's public key
|
|
1211
|
+
*/
|
|
1212
|
+
async unshareFile(filePath, contactPublicKey) {
|
|
1213
|
+
const aleph = this.core.getAlephService();
|
|
1214
|
+
try {
|
|
1215
|
+
const file = await this.getFile(filePath);
|
|
1216
|
+
await aleph.updatePost(
|
|
1217
|
+
POST_TYPES.FILE,
|
|
1218
|
+
file.post_hash,
|
|
1219
|
+
[this.core.getMainAddress()],
|
|
1220
|
+
FileMetaEncryptedSchema,
|
|
1221
|
+
async (encryptedMeta) => {
|
|
1222
|
+
const decryptedMeta = await this.decryptFileMeta(encryptedMeta);
|
|
1223
|
+
delete decryptedMeta.shared_keys[contactPublicKey];
|
|
1224
|
+
return await this.encryptFileMeta(decryptedMeta);
|
|
1225
|
+
}
|
|
1226
|
+
);
|
|
1227
|
+
await aleph.updateAggregate(
|
|
1228
|
+
AGGREGATE_KEYS.FILE_ENTRIES,
|
|
1229
|
+
FileEntriesAggregateSchema,
|
|
1230
|
+
async (aggregate) => ({
|
|
1231
|
+
files: aggregate.files.map(
|
|
1232
|
+
(entry) => entry.post_hash === file.post_hash ? { ...entry, shared_with: entry.shared_with.filter((pk) => pk !== contactPublicKey) } : entry
|
|
1233
|
+
)
|
|
1234
|
+
})
|
|
1235
|
+
);
|
|
1236
|
+
} catch (error) {
|
|
1237
|
+
throw new FileError(`Failed to unshare file: ${error.message}`);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* Share a file publicly (unencrypted, anyone can access)
|
|
1242
|
+
* @param fileInfo - File to share publicly
|
|
1243
|
+
* @param username - Username for attribution
|
|
1244
|
+
* @returns Public post hash for sharing
|
|
1245
|
+
*/
|
|
1246
|
+
async shareFilePublicly(fileInfo, username) {
|
|
1247
|
+
const aleph = this.core.getAlephService();
|
|
1248
|
+
try {
|
|
1249
|
+
const decryptedContent = await this.downloadFile(fileInfo);
|
|
1250
|
+
const storeResult = await aleph.uploadFile(decryptedContent);
|
|
1251
|
+
const publicMeta = {
|
|
1252
|
+
name: fileInfo.name,
|
|
1253
|
+
size: fileInfo.size,
|
|
1254
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1255
|
+
store_hash: storeResult.item_hash,
|
|
1256
|
+
username
|
|
1257
|
+
};
|
|
1258
|
+
const postResult = await aleph.createPost(POST_TYPES.PUBLIC_FILE, publicMeta);
|
|
1259
|
+
return postResult.item_hash;
|
|
1260
|
+
} catch (error) {
|
|
1261
|
+
throw new FileError(`Failed to share file publicly: ${error.message}`);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* Fetch public file metadata (static - no auth required)
|
|
1266
|
+
* @param postHash - Public post hash
|
|
1267
|
+
* @returns Public file metadata or null if not found
|
|
1268
|
+
*/
|
|
1269
|
+
static async fetchPublicFileMeta(postHash) {
|
|
1270
|
+
try {
|
|
1271
|
+
const client = new AlephHttpClient("https://api2.aleph.im");
|
|
1272
|
+
const post = await client.getPost({
|
|
1273
|
+
channels: [ALEPH_GENERAL_CHANNEL],
|
|
1274
|
+
types: [POST_TYPES.PUBLIC_FILE],
|
|
1275
|
+
hashes: [postHash]
|
|
1276
|
+
});
|
|
1277
|
+
return PublicFileMetaSchema.parse(post.content);
|
|
1278
|
+
} catch {
|
|
1279
|
+
return null;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Download public file (static - no auth required)
|
|
1284
|
+
* @param storeHash - Store hash from public metadata
|
|
1285
|
+
* @returns File content as ArrayBuffer
|
|
1286
|
+
*/
|
|
1287
|
+
static async downloadPublicFile(storeHash) {
|
|
1288
|
+
try {
|
|
1289
|
+
const client = new AlephHttpClient("https://api2.aleph.im");
|
|
1290
|
+
return await client.downloadFile(storeHash);
|
|
1291
|
+
} catch (error) {
|
|
1292
|
+
throw new FileError(`Failed to download public file: ${error.message}`);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
// ============================================================================
|
|
1296
|
+
// Private helper methods
|
|
1297
|
+
// ============================================================================
|
|
1298
|
+
async saveFileEntries(files) {
|
|
1299
|
+
const aleph = this.core.getAlephService();
|
|
1300
|
+
await aleph.updateAggregate(
|
|
1301
|
+
AGGREGATE_KEYS.FILE_ENTRIES,
|
|
1302
|
+
FileEntriesAggregateSchema,
|
|
1303
|
+
async (aggregate) => {
|
|
1304
|
+
const newEntries = files.map((f) => ({
|
|
1305
|
+
path: f.path,
|
|
1306
|
+
post_hash: f.post_hash,
|
|
1307
|
+
shared_with: f.shared_with || []
|
|
1308
|
+
}));
|
|
1309
|
+
return { files: [...aggregate.files, ...newEntries] };
|
|
1310
|
+
}
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
async encryptFileMeta(meta) {
|
|
1314
|
+
const key = this.core.getEncryptionKey();
|
|
1315
|
+
const iv = EncryptionService.generateIv();
|
|
1316
|
+
const publicKey = this.core.getPublicKey();
|
|
1317
|
+
return {
|
|
1318
|
+
name: await EncryptionService.encrypt(meta.name, key, iv),
|
|
1319
|
+
path: EncryptionService.encryptEcies(meta.path, publicKey),
|
|
1320
|
+
key: EncryptionService.encryptEcies(meta.key, publicKey),
|
|
1321
|
+
iv: EncryptionService.encryptEcies(meta.iv, publicKey),
|
|
1322
|
+
store_hash: meta.store_hash,
|
|
1323
|
+
size: await EncryptionService.encrypt(meta.size.toString(), key, iv),
|
|
1324
|
+
created_at: await EncryptionService.encrypt(meta.created_at, key, iv),
|
|
1325
|
+
deleted_at: meta.deleted_at ? await EncryptionService.encrypt(meta.deleted_at, key, iv) : null,
|
|
1326
|
+
shared_keys: meta.shared_keys
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
async decryptFileMeta(encryptedMeta, privateKey) {
|
|
1330
|
+
const key = this.core.getEncryptionKey();
|
|
1331
|
+
const iv = EncryptionService.generateIv();
|
|
1332
|
+
const privKey = privateKey || this.core.getSubAccountPrivateKey();
|
|
1333
|
+
if (!privKey) {
|
|
1334
|
+
throw new EncryptionError("Private key not available");
|
|
1335
|
+
}
|
|
1336
|
+
return {
|
|
1337
|
+
name: await EncryptionService.decrypt(encryptedMeta.name, key, iv),
|
|
1338
|
+
path: EncryptionService.decryptEcies(encryptedMeta.path, privKey),
|
|
1339
|
+
key: EncryptionService.decryptEcies(encryptedMeta.key, privKey),
|
|
1340
|
+
iv: EncryptionService.decryptEcies(encryptedMeta.iv, privKey),
|
|
1341
|
+
store_hash: encryptedMeta.store_hash,
|
|
1342
|
+
size: parseInt(await EncryptionService.decrypt(encryptedMeta.size, key, iv)),
|
|
1343
|
+
created_at: await EncryptionService.decrypt(encryptedMeta.created_at, key, iv),
|
|
1344
|
+
deleted_at: encryptedMeta.deleted_at ? await EncryptionService.decrypt(encryptedMeta.deleted_at, key, iv) : null,
|
|
1345
|
+
shared_keys: encryptedMeta.shared_keys || {}
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
};
|
|
1349
|
+
|
|
1350
|
+
// src/services/contact-service.ts
|
|
1351
|
+
var ContactService = class {
|
|
1352
|
+
constructor(core, fileService) {
|
|
1353
|
+
this.core = core;
|
|
1354
|
+
this.fileService = fileService;
|
|
1355
|
+
}
|
|
1356
|
+
/**
|
|
1357
|
+
* Initialize contacts aggregate if it doesn't exist
|
|
1358
|
+
*/
|
|
1359
|
+
async setup() {
|
|
1360
|
+
const aleph = this.core.getAlephService();
|
|
1361
|
+
try {
|
|
1362
|
+
await aleph.fetchAggregate(
|
|
1363
|
+
AGGREGATE_KEYS.CONTACTS,
|
|
1364
|
+
ContactsAggregateSchema
|
|
1365
|
+
);
|
|
1366
|
+
} catch {
|
|
1367
|
+
await aleph.createAggregate(AGGREGATE_KEYS.CONTACTS, { contacts: [] });
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* Fetch all contacts
|
|
1372
|
+
*/
|
|
1373
|
+
async listContacts() {
|
|
1374
|
+
const aleph = this.core.getAlephService();
|
|
1375
|
+
try {
|
|
1376
|
+
const aggregate = await aleph.fetchAggregate(
|
|
1377
|
+
AGGREGATE_KEYS.CONTACTS,
|
|
1378
|
+
ContactsAggregateSchema
|
|
1379
|
+
);
|
|
1380
|
+
return aggregate.contacts;
|
|
1381
|
+
} catch (error) {
|
|
1382
|
+
throw new ContactError(`Failed to fetch contacts: ${error.message}`);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Get a contact by public key
|
|
1387
|
+
* @param publicKey - Contact's public key
|
|
1388
|
+
*/
|
|
1389
|
+
async getContact(publicKey) {
|
|
1390
|
+
const contacts = await this.listContacts();
|
|
1391
|
+
const contact = contacts.find((c) => c.public_key === publicKey);
|
|
1392
|
+
if (!contact) {
|
|
1393
|
+
throw new ContactError(`Contact not found: ${publicKey}`);
|
|
1394
|
+
}
|
|
1395
|
+
return contact;
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* Get a contact by address
|
|
1399
|
+
* @param address - Contact's Ethereum address
|
|
1400
|
+
*/
|
|
1401
|
+
async getContactByAddress(address) {
|
|
1402
|
+
const contacts = await this.listContacts();
|
|
1403
|
+
const contact = contacts.find((c) => c.address.toLowerCase() === address.toLowerCase());
|
|
1404
|
+
if (!contact) {
|
|
1405
|
+
throw new ContactError(`Contact not found: ${address}`);
|
|
1406
|
+
}
|
|
1407
|
+
return contact;
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Add a new contact
|
|
1411
|
+
* @param name - Contact name
|
|
1412
|
+
* @param address - Contact's Ethereum address
|
|
1413
|
+
* @param publicKey - Contact's public key (hex string)
|
|
1414
|
+
*/
|
|
1415
|
+
async addContact(name, address, publicKey) {
|
|
1416
|
+
const aleph = this.core.getAlephService();
|
|
1417
|
+
try {
|
|
1418
|
+
const contacts = await this.listContacts();
|
|
1419
|
+
const existingContact = contacts.find(
|
|
1420
|
+
(c) => c.public_key === publicKey || c.address.toLowerCase() === address.toLowerCase()
|
|
1421
|
+
);
|
|
1422
|
+
if (existingContact) {
|
|
1423
|
+
throw new ContactError("Contact already exists");
|
|
1424
|
+
}
|
|
1425
|
+
const newContact = {
|
|
1426
|
+
name,
|
|
1427
|
+
address,
|
|
1428
|
+
public_key: publicKey
|
|
1429
|
+
};
|
|
1430
|
+
await aleph.updateAggregate(
|
|
1431
|
+
AGGREGATE_KEYS.CONTACTS,
|
|
1432
|
+
ContactsAggregateSchema,
|
|
1433
|
+
async (aggregate) => ({
|
|
1434
|
+
contacts: [...aggregate.contacts, newContact]
|
|
1435
|
+
})
|
|
1436
|
+
);
|
|
1437
|
+
return newContact;
|
|
1438
|
+
} catch (error) {
|
|
1439
|
+
if (error instanceof ContactError) {
|
|
1440
|
+
throw error;
|
|
1441
|
+
}
|
|
1442
|
+
throw new ContactError(`Failed to add contact: ${error.message}`);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Remove a contact
|
|
1447
|
+
* @param publicKey - Contact's public key
|
|
1448
|
+
*/
|
|
1449
|
+
async removeContact(publicKey) {
|
|
1450
|
+
const aleph = this.core.getAlephService();
|
|
1451
|
+
try {
|
|
1452
|
+
await this.getContact(publicKey);
|
|
1453
|
+
await aleph.updateAggregate(
|
|
1454
|
+
AGGREGATE_KEYS.CONTACTS,
|
|
1455
|
+
ContactsAggregateSchema,
|
|
1456
|
+
async (aggregate) => ({
|
|
1457
|
+
contacts: aggregate.contacts.filter((c) => c.public_key !== publicKey)
|
|
1458
|
+
})
|
|
1459
|
+
);
|
|
1460
|
+
} catch (error) {
|
|
1461
|
+
if (error instanceof ContactError) {
|
|
1462
|
+
throw error;
|
|
1463
|
+
}
|
|
1464
|
+
throw new ContactError(`Failed to remove contact: ${error.message}`);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
/**
|
|
1468
|
+
* Update a contact's name
|
|
1469
|
+
* @param publicKey - Contact's public key
|
|
1470
|
+
* @param newName - New name for the contact
|
|
1471
|
+
*/
|
|
1472
|
+
async updateContactName(publicKey, newName) {
|
|
1473
|
+
const aleph = this.core.getAlephService();
|
|
1474
|
+
try {
|
|
1475
|
+
const existingContact = await this.getContact(publicKey);
|
|
1476
|
+
const updatedContact = {
|
|
1477
|
+
...existingContact,
|
|
1478
|
+
name: newName
|
|
1479
|
+
};
|
|
1480
|
+
await aleph.updateAggregate(
|
|
1481
|
+
AGGREGATE_KEYS.CONTACTS,
|
|
1482
|
+
ContactsAggregateSchema,
|
|
1483
|
+
async (aggregate) => ({
|
|
1484
|
+
contacts: aggregate.contacts.map(
|
|
1485
|
+
(c) => c.public_key === publicKey ? updatedContact : c
|
|
1486
|
+
)
|
|
1487
|
+
})
|
|
1488
|
+
);
|
|
1489
|
+
return updatedContact;
|
|
1490
|
+
} catch (error) {
|
|
1491
|
+
if (error instanceof ContactError) {
|
|
1492
|
+
throw error;
|
|
1493
|
+
}
|
|
1494
|
+
throw new ContactError(`Failed to update contact: ${error.message}`);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
/**
|
|
1498
|
+
* Fetch files shared by a contact
|
|
1499
|
+
* @param publicKey - Contact's public key
|
|
1500
|
+
*/
|
|
1501
|
+
async getSharedFiles(publicKey) {
|
|
1502
|
+
try {
|
|
1503
|
+
await this.getContact(publicKey);
|
|
1504
|
+
const entries = await this.fileService.fetchFileEntries();
|
|
1505
|
+
const sharedEntries = entries.filter(
|
|
1506
|
+
(entry) => entry.shared_with.includes(publicKey)
|
|
1507
|
+
);
|
|
1508
|
+
const files = await this.fileService.fetchFilesMetaFromEntries(sharedEntries);
|
|
1509
|
+
return files;
|
|
1510
|
+
} catch (error) {
|
|
1511
|
+
throw new ContactError(`Failed to fetch shared files: ${error.message}`);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
/**
|
|
1515
|
+
* Fetch files that a contact has shared with the current user
|
|
1516
|
+
* @param publicKey - Contact's public key
|
|
1517
|
+
* @returns Files shared by the contact with current user
|
|
1518
|
+
*/
|
|
1519
|
+
async fetchFilesSharedByContact(publicKey) {
|
|
1520
|
+
try {
|
|
1521
|
+
const contact = await this.getContact(publicKey);
|
|
1522
|
+
const currentUserPublicKey = this.core.getPublicKey();
|
|
1523
|
+
const aleph = this.core.getAlephService();
|
|
1524
|
+
const contactEntries = await aleph.fetchAggregate(
|
|
1525
|
+
AGGREGATE_KEYS.FILE_ENTRIES,
|
|
1526
|
+
FileEntriesAggregateSchema,
|
|
1527
|
+
contact.address
|
|
1528
|
+
// Important: contact's address, not current user's
|
|
1529
|
+
);
|
|
1530
|
+
const sharedEntries = contactEntries.files.filter(
|
|
1531
|
+
(entry) => entry.shared_with.includes(currentUserPublicKey)
|
|
1532
|
+
);
|
|
1533
|
+
const files = await this.fileService.fetchFilesMetaFromEntries(
|
|
1534
|
+
sharedEntries,
|
|
1535
|
+
contact.address
|
|
1536
|
+
// Owner is the contact
|
|
1537
|
+
);
|
|
1538
|
+
return files;
|
|
1539
|
+
} catch (error) {
|
|
1540
|
+
throw new ContactError(`Failed to fetch files shared by contact: ${error.message}`);
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
/**
|
|
1544
|
+
* Share a file with a contact
|
|
1545
|
+
* @param filePath - Path of the file to share
|
|
1546
|
+
* @param publicKey - Contact's public key
|
|
1547
|
+
*/
|
|
1548
|
+
async shareFileWithContact(filePath, publicKey) {
|
|
1549
|
+
try {
|
|
1550
|
+
await this.getContact(publicKey);
|
|
1551
|
+
await this.fileService.shareFile(filePath, publicKey);
|
|
1552
|
+
} catch (error) {
|
|
1553
|
+
throw new ContactError(`Failed to share file with contact: ${error.message}`);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
/**
|
|
1557
|
+
* Unshare a file with a contact
|
|
1558
|
+
* @param filePath - Path of the file to unshare
|
|
1559
|
+
* @param publicKey - Contact's public key
|
|
1560
|
+
*/
|
|
1561
|
+
async unshareFileWithContact(filePath, publicKey) {
|
|
1562
|
+
try {
|
|
1563
|
+
await this.getContact(publicKey);
|
|
1564
|
+
await this.fileService.unshareFile(filePath, publicKey);
|
|
1565
|
+
} catch (error) {
|
|
1566
|
+
throw new ContactError(`Failed to unshare file with contact: ${error.message}`);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
};
|
|
1570
|
+
|
|
1571
|
+
// src/services/knowledge-base-service.ts
|
|
1572
|
+
var KnowledgeBaseService = class {
|
|
1573
|
+
constructor(core) {
|
|
1574
|
+
this.core = core;
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Initialize knowledge bases aggregate if it doesn't exist
|
|
1578
|
+
*/
|
|
1579
|
+
async setup() {
|
|
1580
|
+
const aleph = this.core.getAlephService();
|
|
1581
|
+
try {
|
|
1582
|
+
await aleph.fetchAggregate(
|
|
1583
|
+
AGGREGATE_KEYS.KNOWLEDGE_BASES,
|
|
1584
|
+
KnowledgeBasesAggregateSchema
|
|
1585
|
+
);
|
|
1586
|
+
} catch {
|
|
1587
|
+
await aleph.createAggregate(AGGREGATE_KEYS.KNOWLEDGE_BASES, { knowledge_bases: [] });
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
/**
|
|
1591
|
+
* Fetch all knowledge bases
|
|
1592
|
+
*/
|
|
1593
|
+
async listKnowledgeBases() {
|
|
1594
|
+
const aleph = this.core.getAlephService();
|
|
1595
|
+
try {
|
|
1596
|
+
const aggregate = await aleph.fetchAggregate(
|
|
1597
|
+
AGGREGATE_KEYS.KNOWLEDGE_BASES,
|
|
1598
|
+
KnowledgeBasesAggregateSchema
|
|
1599
|
+
);
|
|
1600
|
+
return aggregate.knowledge_bases;
|
|
1601
|
+
} catch (error) {
|
|
1602
|
+
throw new KnowledgeBaseError(`Failed to fetch knowledge bases: ${error.message}`);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* Get a knowledge base by name
|
|
1607
|
+
* @param name - Knowledge base name
|
|
1608
|
+
*/
|
|
1609
|
+
async getKnowledgeBase(name) {
|
|
1610
|
+
const kbs = await this.listKnowledgeBases();
|
|
1611
|
+
const kb = kbs.find((k) => k.name === name);
|
|
1612
|
+
if (!kb) {
|
|
1613
|
+
throw new KnowledgeBaseError(`Knowledge base not found: ${name}`);
|
|
1614
|
+
}
|
|
1615
|
+
return kb;
|
|
1616
|
+
}
|
|
1617
|
+
/**
|
|
1618
|
+
* Create a new knowledge base
|
|
1619
|
+
* @param name - Knowledge base name
|
|
1620
|
+
* @param filePaths - Optional initial file paths
|
|
1621
|
+
*/
|
|
1622
|
+
async createKnowledgeBase(name, filePaths = []) {
|
|
1623
|
+
const aleph = this.core.getAlephService();
|
|
1624
|
+
try {
|
|
1625
|
+
const kbs = await this.listKnowledgeBases();
|
|
1626
|
+
const existingKb = kbs.find((k) => k.name === name);
|
|
1627
|
+
if (existingKb) {
|
|
1628
|
+
throw new KnowledgeBaseError(`Knowledge base already exists: ${name}`);
|
|
1629
|
+
}
|
|
1630
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1631
|
+
const newKb = {
|
|
1632
|
+
name,
|
|
1633
|
+
file_paths: filePaths,
|
|
1634
|
+
created_at: now,
|
|
1635
|
+
updated_at: now
|
|
1636
|
+
};
|
|
1637
|
+
await aleph.updateAggregate(
|
|
1638
|
+
AGGREGATE_KEYS.KNOWLEDGE_BASES,
|
|
1639
|
+
KnowledgeBasesAggregateSchema,
|
|
1640
|
+
async (aggregate) => ({
|
|
1641
|
+
knowledge_bases: [...aggregate.knowledge_bases, newKb]
|
|
1642
|
+
})
|
|
1643
|
+
);
|
|
1644
|
+
return newKb;
|
|
1645
|
+
} catch (error) {
|
|
1646
|
+
if (error instanceof KnowledgeBaseError) {
|
|
1647
|
+
throw error;
|
|
1648
|
+
}
|
|
1649
|
+
throw new KnowledgeBaseError(`Failed to create knowledge base: ${error.message}`);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
/**
|
|
1653
|
+
* Delete a knowledge base
|
|
1654
|
+
* @param name - Knowledge base name
|
|
1655
|
+
*/
|
|
1656
|
+
async deleteKnowledgeBase(name) {
|
|
1657
|
+
const aleph = this.core.getAlephService();
|
|
1658
|
+
try {
|
|
1659
|
+
await this.getKnowledgeBase(name);
|
|
1660
|
+
await aleph.updateAggregate(
|
|
1661
|
+
AGGREGATE_KEYS.KNOWLEDGE_BASES,
|
|
1662
|
+
KnowledgeBasesAggregateSchema,
|
|
1663
|
+
async (aggregate) => ({
|
|
1664
|
+
knowledge_bases: aggregate.knowledge_bases.filter((k) => k.name !== name)
|
|
1665
|
+
})
|
|
1666
|
+
);
|
|
1667
|
+
} catch (error) {
|
|
1668
|
+
if (error instanceof KnowledgeBaseError) {
|
|
1669
|
+
throw error;
|
|
1670
|
+
}
|
|
1671
|
+
throw new KnowledgeBaseError(`Failed to delete knowledge base: ${error.message}`);
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
/**
|
|
1675
|
+
* Rename a knowledge base
|
|
1676
|
+
* @param oldName - Current name
|
|
1677
|
+
* @param newName - New name
|
|
1678
|
+
*/
|
|
1679
|
+
async renameKnowledgeBase(oldName, newName) {
|
|
1680
|
+
const aleph = this.core.getAlephService();
|
|
1681
|
+
try {
|
|
1682
|
+
const existingKb = await this.getKnowledgeBase(oldName);
|
|
1683
|
+
const kbs = await this.listKnowledgeBases();
|
|
1684
|
+
if (kbs.some((k) => k.name === newName)) {
|
|
1685
|
+
throw new KnowledgeBaseError(`Knowledge base already exists: ${newName}`);
|
|
1686
|
+
}
|
|
1687
|
+
const updatedKb = {
|
|
1688
|
+
...existingKb,
|
|
1689
|
+
name: newName,
|
|
1690
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1691
|
+
};
|
|
1692
|
+
await aleph.updateAggregate(
|
|
1693
|
+
AGGREGATE_KEYS.KNOWLEDGE_BASES,
|
|
1694
|
+
KnowledgeBasesAggregateSchema,
|
|
1695
|
+
async (aggregate) => ({
|
|
1696
|
+
knowledge_bases: aggregate.knowledge_bases.map(
|
|
1697
|
+
(k) => k.name === oldName ? updatedKb : k
|
|
1698
|
+
)
|
|
1699
|
+
})
|
|
1700
|
+
);
|
|
1701
|
+
return updatedKb;
|
|
1702
|
+
} catch (error) {
|
|
1703
|
+
if (error instanceof KnowledgeBaseError) {
|
|
1704
|
+
throw error;
|
|
1705
|
+
}
|
|
1706
|
+
throw new KnowledgeBaseError(`Failed to rename knowledge base: ${error.message}`);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
/**
|
|
1710
|
+
* Set the files in a knowledge base (replaces all existing files)
|
|
1711
|
+
* @param name - Knowledge base name
|
|
1712
|
+
* @param filePaths - File paths
|
|
1713
|
+
*/
|
|
1714
|
+
async setFiles(name, filePaths) {
|
|
1715
|
+
const aleph = this.core.getAlephService();
|
|
1716
|
+
try {
|
|
1717
|
+
const existingKb = await this.getKnowledgeBase(name);
|
|
1718
|
+
const updatedKb = {
|
|
1719
|
+
...existingKb,
|
|
1720
|
+
file_paths: filePaths,
|
|
1721
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1722
|
+
};
|
|
1723
|
+
await aleph.updateAggregate(
|
|
1724
|
+
AGGREGATE_KEYS.KNOWLEDGE_BASES,
|
|
1725
|
+
KnowledgeBasesAggregateSchema,
|
|
1726
|
+
async (aggregate) => ({
|
|
1727
|
+
knowledge_bases: aggregate.knowledge_bases.map(
|
|
1728
|
+
(k) => k.name === name ? updatedKb : k
|
|
1729
|
+
)
|
|
1730
|
+
})
|
|
1731
|
+
);
|
|
1732
|
+
return updatedKb;
|
|
1733
|
+
} catch (error) {
|
|
1734
|
+
if (error instanceof KnowledgeBaseError) {
|
|
1735
|
+
throw error;
|
|
1736
|
+
}
|
|
1737
|
+
throw new KnowledgeBaseError(`Failed to set files: ${error.message}`);
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
/**
|
|
1741
|
+
* Add files to a knowledge base
|
|
1742
|
+
* @param name - Knowledge base name
|
|
1743
|
+
* @param filePaths - File paths to add
|
|
1744
|
+
*/
|
|
1745
|
+
async addFiles(name, filePaths) {
|
|
1746
|
+
const aleph = this.core.getAlephService();
|
|
1747
|
+
try {
|
|
1748
|
+
const existingKb = await this.getKnowledgeBase(name);
|
|
1749
|
+
const updatedFilePaths = [.../* @__PURE__ */ new Set([...existingKb.file_paths, ...filePaths])];
|
|
1750
|
+
const updatedKb = {
|
|
1751
|
+
...existingKb,
|
|
1752
|
+
file_paths: updatedFilePaths,
|
|
1753
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1754
|
+
};
|
|
1755
|
+
await aleph.updateAggregate(
|
|
1756
|
+
AGGREGATE_KEYS.KNOWLEDGE_BASES,
|
|
1757
|
+
KnowledgeBasesAggregateSchema,
|
|
1758
|
+
async (aggregate) => ({
|
|
1759
|
+
knowledge_bases: aggregate.knowledge_bases.map(
|
|
1760
|
+
(k) => k.name === name ? updatedKb : k
|
|
1761
|
+
)
|
|
1762
|
+
})
|
|
1763
|
+
);
|
|
1764
|
+
return updatedKb;
|
|
1765
|
+
} catch (error) {
|
|
1766
|
+
if (error instanceof KnowledgeBaseError) {
|
|
1767
|
+
throw error;
|
|
1768
|
+
}
|
|
1769
|
+
throw new KnowledgeBaseError(`Failed to add files: ${error.message}`);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
/**
|
|
1773
|
+
* Remove files from a knowledge base
|
|
1774
|
+
* @param name - Knowledge base name
|
|
1775
|
+
* @param filePaths - File paths to remove
|
|
1776
|
+
*/
|
|
1777
|
+
async removeFiles(name, filePaths) {
|
|
1778
|
+
const aleph = this.core.getAlephService();
|
|
1779
|
+
try {
|
|
1780
|
+
const existingKb = await this.getKnowledgeBase(name);
|
|
1781
|
+
const updatedFilePaths = existingKb.file_paths.filter(
|
|
1782
|
+
(path) => !filePaths.includes(path)
|
|
1783
|
+
);
|
|
1784
|
+
const updatedKb = {
|
|
1785
|
+
...existingKb,
|
|
1786
|
+
file_paths: updatedFilePaths,
|
|
1787
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1788
|
+
};
|
|
1789
|
+
await aleph.updateAggregate(
|
|
1790
|
+
AGGREGATE_KEYS.KNOWLEDGE_BASES,
|
|
1791
|
+
KnowledgeBasesAggregateSchema,
|
|
1792
|
+
async (aggregate) => ({
|
|
1793
|
+
knowledge_bases: aggregate.knowledge_bases.map(
|
|
1794
|
+
(k) => k.name === name ? updatedKb : k
|
|
1795
|
+
)
|
|
1796
|
+
})
|
|
1797
|
+
);
|
|
1798
|
+
return updatedKb;
|
|
1799
|
+
} catch (error) {
|
|
1800
|
+
if (error instanceof KnowledgeBaseError) {
|
|
1801
|
+
throw error;
|
|
1802
|
+
}
|
|
1803
|
+
throw new KnowledgeBaseError(`Failed to remove files: ${error.message}`);
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
/**
|
|
1807
|
+
* Clear all files from a knowledge base
|
|
1808
|
+
* @param name - Knowledge base name
|
|
1809
|
+
*/
|
|
1810
|
+
async clearFiles(name) {
|
|
1811
|
+
return await this.setFiles(name, []);
|
|
1812
|
+
}
|
|
1813
|
+
};
|
|
1814
|
+
|
|
1815
|
+
// src/services/credit-service.ts
|
|
1816
|
+
var _CreditService = class _CreditService {
|
|
1817
|
+
constructor(core) {
|
|
1818
|
+
this.core = core;
|
|
1819
|
+
}
|
|
1820
|
+
/**
|
|
1821
|
+
* Get user's credit balance and transaction history
|
|
1822
|
+
* @returns User credit data with balance and transactions
|
|
1823
|
+
*/
|
|
1824
|
+
async getCreditBalance() {
|
|
1825
|
+
const aleph = this.core.getAlephService();
|
|
1826
|
+
const userAddress = this.core.getMainAddress();
|
|
1827
|
+
try {
|
|
1828
|
+
const creditAggregate = await aleph.fetchAggregate(
|
|
1829
|
+
AGGREGATE_KEYS.CREDITS,
|
|
1830
|
+
CreditAggregateSchema,
|
|
1831
|
+
_CreditService.BACKEND_ADDRESS
|
|
1832
|
+
);
|
|
1833
|
+
return creditAggregate[userAddress] || { balance: 0, transactions: [] };
|
|
1834
|
+
} catch {
|
|
1835
|
+
return { balance: 0, transactions: [] };
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
};
|
|
1839
|
+
_CreditService.BACKEND_ADDRESS = "0x1234567890123456789012345678901234567890";
|
|
1840
|
+
var CreditService = _CreditService;
|
|
1841
|
+
|
|
1842
|
+
// src/bedrock-client.ts
|
|
1843
|
+
var BedrockClient = class _BedrockClient {
|
|
1844
|
+
constructor(core) {
|
|
1845
|
+
this.core = core;
|
|
1846
|
+
this.files = new FileService(core);
|
|
1847
|
+
this.contacts = new ContactService(core, this.files);
|
|
1848
|
+
this.knowledgeBases = new KnowledgeBaseService(core);
|
|
1849
|
+
this.credits = new CreditService(core);
|
|
1850
|
+
}
|
|
1851
|
+
/**
|
|
1852
|
+
* Create BedrockClient from a private key
|
|
1853
|
+
*
|
|
1854
|
+
* @param privateKey - Ethereum private key (hex string with or without 0x prefix)
|
|
1855
|
+
* @param config - Optional configuration
|
|
1856
|
+
* @returns Initialized BedrockClient
|
|
1857
|
+
*
|
|
1858
|
+
* @example
|
|
1859
|
+
* ```typescript
|
|
1860
|
+
* const client = await BedrockClient.fromPrivateKey('0xabc123...');
|
|
1861
|
+
* ```
|
|
1862
|
+
*/
|
|
1863
|
+
static async fromPrivateKey(privateKey, config) {
|
|
1864
|
+
const core = await BedrockCore.fromPrivateKey(privateKey, config);
|
|
1865
|
+
const client = new _BedrockClient(core);
|
|
1866
|
+
await client.setup();
|
|
1867
|
+
return client;
|
|
1868
|
+
}
|
|
1869
|
+
/**
|
|
1870
|
+
* Create BedrockClient from a wallet provider (e.g., MetaMask)
|
|
1871
|
+
*
|
|
1872
|
+
* @param provider - EIP-1193 provider
|
|
1873
|
+
* @param config - Optional configuration
|
|
1874
|
+
* @returns Initialized BedrockClient
|
|
1875
|
+
*
|
|
1876
|
+
* @example
|
|
1877
|
+
* ```typescript
|
|
1878
|
+
* const client = await BedrockClient.fromProvider(window.ethereum);
|
|
1879
|
+
* ```
|
|
1880
|
+
*/
|
|
1881
|
+
static async fromProvider(provider, config) {
|
|
1882
|
+
const core = await BedrockCore.fromProvider(provider, config);
|
|
1883
|
+
const client = new _BedrockClient(core);
|
|
1884
|
+
await client.setup();
|
|
1885
|
+
return client;
|
|
1886
|
+
}
|
|
1887
|
+
/**
|
|
1888
|
+
* Create BedrockClient from a signature hash
|
|
1889
|
+
*
|
|
1890
|
+
* @param signatureHash - Signature hash from wallet
|
|
1891
|
+
* @param provider - EIP-1193 provider
|
|
1892
|
+
* @param config - Optional configuration
|
|
1893
|
+
* @returns Initialized BedrockClient
|
|
1894
|
+
*
|
|
1895
|
+
* @example
|
|
1896
|
+
* ```typescript
|
|
1897
|
+
* const signature = await wallet.signMessage({ message: 'Bedrock.im' });
|
|
1898
|
+
* const client = await BedrockClient.fromSignature(signature, window.ethereum);
|
|
1899
|
+
* ```
|
|
1900
|
+
*/
|
|
1901
|
+
static async fromSignature(signatureHash, provider, config) {
|
|
1902
|
+
const core = await BedrockCore.fromSignature(signatureHash, provider, config);
|
|
1903
|
+
const client = new _BedrockClient(core);
|
|
1904
|
+
await client.setup();
|
|
1905
|
+
return client;
|
|
1906
|
+
}
|
|
1907
|
+
/**
|
|
1908
|
+
* Get the main account address
|
|
1909
|
+
*/
|
|
1910
|
+
getMainAddress() {
|
|
1911
|
+
return this.core.getMainAddress();
|
|
1912
|
+
}
|
|
1913
|
+
/**
|
|
1914
|
+
* Get the sub-account address
|
|
1915
|
+
*/
|
|
1916
|
+
getSubAddress() {
|
|
1917
|
+
return this.core.getSubAddress();
|
|
1918
|
+
}
|
|
1919
|
+
/**
|
|
1920
|
+
* Get the account's public key
|
|
1921
|
+
*/
|
|
1922
|
+
getPublicKey() {
|
|
1923
|
+
return this.core.getPublicKey();
|
|
1924
|
+
}
|
|
1925
|
+
/**
|
|
1926
|
+
* Get the encryption key (32 bytes derived from signature)
|
|
1927
|
+
*/
|
|
1928
|
+
getEncryptionKey() {
|
|
1929
|
+
return this.core.getEncryptionKey();
|
|
1930
|
+
}
|
|
1931
|
+
/**
|
|
1932
|
+
* Reset all data (files, contacts, knowledge bases)
|
|
1933
|
+
* WARNING: This will delete all user data from Aleph
|
|
1934
|
+
*/
|
|
1935
|
+
async resetAllData() {
|
|
1936
|
+
await Promise.all([
|
|
1937
|
+
this.resetFiles(),
|
|
1938
|
+
this.resetContacts(),
|
|
1939
|
+
this.resetKnowledgeBases()
|
|
1940
|
+
]);
|
|
1941
|
+
}
|
|
1942
|
+
/**
|
|
1943
|
+
* Reset all files
|
|
1944
|
+
* WARNING: This will delete all files from Aleph
|
|
1945
|
+
*/
|
|
1946
|
+
async resetFiles() {
|
|
1947
|
+
const aleph = this.core.getAlephService();
|
|
1948
|
+
await aleph.createAggregate(AGGREGATE_KEYS.FILE_ENTRIES, []);
|
|
1949
|
+
}
|
|
1950
|
+
/**
|
|
1951
|
+
* Reset all contacts
|
|
1952
|
+
* WARNING: This will delete all contacts
|
|
1953
|
+
*/
|
|
1954
|
+
async resetContacts() {
|
|
1955
|
+
const aleph = this.core.getAlephService();
|
|
1956
|
+
await aleph.createAggregate(AGGREGATE_KEYS.CONTACTS, []);
|
|
1957
|
+
}
|
|
1958
|
+
/**
|
|
1959
|
+
* Reset all knowledge bases
|
|
1960
|
+
* WARNING: This will delete all knowledge bases
|
|
1961
|
+
*/
|
|
1962
|
+
async resetKnowledgeBases() {
|
|
1963
|
+
const aleph = this.core.getAlephService();
|
|
1964
|
+
await aleph.createAggregate(AGGREGATE_KEYS.KNOWLEDGE_BASES, []);
|
|
1965
|
+
}
|
|
1966
|
+
// ============================================================================
|
|
1967
|
+
// Private methods
|
|
1968
|
+
// ============================================================================
|
|
1969
|
+
/**
|
|
1970
|
+
* Setup all services (create aggregates if they don't exist)
|
|
1971
|
+
*/
|
|
1972
|
+
async setup() {
|
|
1973
|
+
await Promise.all([
|
|
1974
|
+
this.files.setup(),
|
|
1975
|
+
this.contacts.setup(),
|
|
1976
|
+
this.knowledgeBases.setup()
|
|
1977
|
+
]);
|
|
1978
|
+
}
|
|
1979
|
+
};
|
|
1980
|
+
|
|
1981
|
+
// src/index.ts
|
|
1982
|
+
import { ETHAccount as ETHAccount2 } from "@aleph-sdk/ethereum";
|
|
1983
|
+
import { AlephHttpClient as AlephHttpClient2, AuthenticatedAlephHttpClient as AuthenticatedAlephHttpClient3 } from "@aleph-sdk/client";
|
|
1984
|
+
export {
|
|
1985
|
+
AGGREGATE_KEYS,
|
|
1986
|
+
ALEPH_GENERAL_CHANNEL,
|
|
1987
|
+
AddressSchema,
|
|
1988
|
+
AlephHttpClient2 as AlephHttpClient,
|
|
1989
|
+
AlephService,
|
|
1990
|
+
AuthenticatedAlephHttpClient3 as AuthenticatedAlephHttpClient,
|
|
1991
|
+
AuthenticationError,
|
|
1992
|
+
BEDROCK_MESSAGE,
|
|
1993
|
+
BedrockClient,
|
|
1994
|
+
BedrockCore,
|
|
1995
|
+
BedrockError,
|
|
1996
|
+
ContactError,
|
|
1997
|
+
ContactSchema,
|
|
1998
|
+
ContactService,
|
|
1999
|
+
ContactsAggregateSchema,
|
|
2000
|
+
CreditAggregateSchema,
|
|
2001
|
+
CreditError,
|
|
2002
|
+
CreditService,
|
|
2003
|
+
CreditTransactionSchema,
|
|
2004
|
+
CryptoUtils,
|
|
2005
|
+
DatetimeSchema,
|
|
2006
|
+
ETHAccount2 as ETHAccount,
|
|
2007
|
+
EncryptionError,
|
|
2008
|
+
EncryptionService,
|
|
2009
|
+
FileEntriesAggregateSchema,
|
|
2010
|
+
FileEntrySchema,
|
|
2011
|
+
FileError,
|
|
2012
|
+
FileMetaEncryptedSchema,
|
|
2013
|
+
FileMetaSchema,
|
|
2014
|
+
FileNotFoundError,
|
|
2015
|
+
FileService,
|
|
2016
|
+
HexString32Schema,
|
|
2017
|
+
HexString64Schema,
|
|
2018
|
+
KnowledgeBaseError,
|
|
2019
|
+
KnowledgeBaseSchema,
|
|
2020
|
+
KnowledgeBaseService,
|
|
2021
|
+
KnowledgeBasesAggregateSchema,
|
|
2022
|
+
NetworkError,
|
|
2023
|
+
POST_TYPES,
|
|
2024
|
+
PublicFileMetaSchema,
|
|
2025
|
+
SECURITY_AGGREGATE_KEY,
|
|
2026
|
+
SecurityAggregateSchema,
|
|
2027
|
+
UserCreditSchema,
|
|
2028
|
+
ValidationError
|
|
2029
|
+
};
|
|
2030
|
+
//# sourceMappingURL=index.mjs.map
|