@secrecy/lib 1.74.0-feat-groups-identity.4 → 1.74.0-feat-transfer-adaptations.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.
Files changed (54) hide show
  1. package/dist/lib/base-client.js +4 -27
  2. package/dist/lib/client/SecrecyAppClient.js +17 -13
  3. package/dist/lib/client/SecrecyCloudClient.js +156 -368
  4. package/dist/lib/client/SecrecyDbClient.js +7 -3
  5. package/dist/lib/client/SecrecyMailClient.js +48 -38
  6. package/dist/lib/client/SecrecyOrganizationClient.js +12 -10
  7. package/dist/lib/client/SecrecyPayClient.js +5 -1
  8. package/dist/lib/client/SecrecyPseudonymClient.js +8 -4
  9. package/dist/lib/client/SecrecyUserClient.js +11 -11
  10. package/dist/lib/client/SecrecyWalletClient.js +2 -0
  11. package/dist/lib/client/convert/data.js +4 -4
  12. package/dist/lib/client/convert/mail.js +6 -5
  13. package/dist/lib/client/convert/node.js +34 -46
  14. package/dist/lib/client/data-link.js +77 -0
  15. package/dist/lib/client/download.js +84 -0
  16. package/dist/lib/client/helpers.js +11 -18
  17. package/dist/lib/client/index.js +12 -48
  18. package/dist/lib/client/storage.js +2 -3
  19. package/dist/lib/client/types/index.js +7 -3
  20. package/dist/lib/client/upload.js +252 -0
  21. package/dist/lib/client.js +7 -0
  22. package/dist/lib/crypto/data.js +3 -0
  23. package/dist/lib/crypto/domain.js +123 -12
  24. package/dist/lib/crypto/helpers.js +23 -0
  25. package/dist/lib/index.js +0 -1
  26. package/dist/types/base-client.d.ts +1 -2
  27. package/dist/types/client/SecrecyAppClient.d.ts +3 -2
  28. package/dist/types/client/SecrecyCloudClient.d.ts +28 -20
  29. package/dist/types/client/SecrecyDbClient.d.ts +3 -1
  30. package/dist/types/client/SecrecyMailClient.d.ts +3 -2
  31. package/dist/types/client/SecrecyOrganizationClient.d.ts +3 -2
  32. package/dist/types/client/SecrecyPayClient.d.ts +3 -1
  33. package/dist/types/client/SecrecyPseudonymClient.d.ts +3 -2
  34. package/dist/types/client/SecrecyUserClient.d.ts +3 -2
  35. package/dist/types/client/convert/data.d.ts +3 -3
  36. package/dist/types/client/convert/mail.d.ts +5 -3
  37. package/dist/types/client/convert/node.d.ts +5 -5
  38. package/dist/types/client/data-link.d.ts +37 -0
  39. package/dist/types/client/download.d.ts +2 -0
  40. package/dist/types/client/index.d.ts +3 -11
  41. package/dist/types/client/storage.d.ts +2 -3
  42. package/dist/types/client/types/index.d.ts +9 -13
  43. package/dist/types/client/types/mail.d.ts +1 -2
  44. package/dist/types/client/types/node.d.ts +9 -12
  45. package/dist/types/client/types/user.d.ts +0 -15
  46. package/dist/types/client/upload.d.ts +38 -0
  47. package/dist/types/client.d.ts +6174 -681
  48. package/dist/types/crypto/domain.d.ts +36 -8
  49. package/dist/types/crypto/helpers.d.ts +12 -0
  50. package/dist/types/crypto/index.d.ts +3 -3
  51. package/dist/types/index.d.ts +1 -2
  52. package/package.json +4 -2
  53. package/dist/lib/client/types/identity.js +0 -18
  54. package/dist/types/client/types/identity.d.ts +0 -29
@@ -1,43 +1,48 @@
1
- import ky from 'ky';
2
1
  import { nodesCache, dataCache, dataContentCache, nodesEncryptionCache, } from '../cache.js';
3
- import { secretStreamKeygen } from '../crypto/data.js';
4
2
  import { decryptCryptoBox, encryptCryptoBox } from '../crypto/index.js';
5
- import { compress, decompress } from '../minify/index.js';
3
+ import { decompress } from '../minify/index.js';
6
4
  import { sodium } from '../sodium.js';
7
- import { enumerate, chunks, concatenate } from '../utils/array.js';
8
5
  import { md5 } from '../worker/md5.js';
9
- import { decrypt, encrypt } from '../worker/sodium.js';
6
+ import { decrypt } from '../worker/sodium.js';
10
7
  import { apiDataToExternal, apiDataToInternal } from './convert/data.js';
11
8
  import { apiNodeForEncryptionToInternal, apiNodeFullToInternalFull, apiNodeToExternal, apiNodeToExternalNodeFull, internalNodeFullToNodeFull, } from './convert/node.js';
12
- import { promiseAllLimit } from '../utils/promise.js';
13
- import { kiloToBytes } from '../utils.js';
14
- import { fileTypeFromBuffer } from 'file-type';
15
- import { encryptName, generateAndEncryptNameAndKey } from '../crypto/domain.js';
9
+ import { buildBytesFromApiData } from '../crypto/domain.js';
16
10
  import { chunkByTotalItems } from '../utils/object.js';
17
- import axios from 'axios';
11
+ import { createPublicDataLink, uploadData } from './upload.js';
12
+ import { encryptName, generateAndEncryptNameAndKey } from '../crypto/helpers.js';
13
+ import { downloadDataFromLink, } from './data-link.js';
18
14
  export class SecrecyCloudClient {
19
15
  #client;
20
- constructor(client) {
16
+ #keys;
17
+ #apiClient;
18
+ constructor(client, keys, apiClient) {
21
19
  this.#client = client;
20
+ this.#keys = keys;
21
+ this.#apiClient = apiClient;
22
22
  }
23
23
  async addDataToHistory({ dataId, nodeId, }) {
24
- const addDataToHistory = await this.#client.apiClient.cloud.addDataToHistory.mutate({
25
- dataId,
26
- nodeId,
27
- });
28
- const node = await apiNodeFullToInternalFull(addDataToHistory, this.#client.keyPairs);
24
+ const addDataToHistory = await this.#apiClient.cloud.addDataToHistory.mutate({ dataId, nodeId });
25
+ const node = await apiNodeFullToInternalFull(addDataToHistory, this.#keys);
29
26
  const data = node.history.find((d) => d.id === dataId);
30
27
  if (data !== undefined) {
31
- const othersIdentities = Object.entries(node.identities).filter(([id]) => id !== this.#client.uaIdentity.identityPubKey);
32
- const shares = othersIdentities.map(([publicKey]) => {
28
+ // TODO sad to have to make this extra request
29
+ const me = await this.#client.me();
30
+ const users = node.users.filter(([u]) => u.id !== me.id);
31
+ const userIds = users.map(([user]) => user.id);
32
+ const userKeys = await this.#client.app.userPublicKey(userIds);
33
+ const shares = users.map(([user]) => {
34
+ const publicKey = userKeys[user.id];
35
+ if (!publicKey) {
36
+ throw new Error('Unable to retrieve share by public key!');
37
+ }
33
38
  return {
34
- pubKey: publicKey,
39
+ id: user.id,
35
40
  key: data.key
36
- ? sodium.to_hex(encryptCryptoBox(sodium.from_hex(data.key), publicKey, this.#client.uaPrivateKey))
41
+ ? sodium.to_hex(encryptCryptoBox(sodium.from_hex(data.key), publicKey, this.#keys.privateKey))
37
42
  : null,
38
43
  };
39
44
  });
40
- await this.#client.apiClient.cloud.shareDataInHistory.mutate({
45
+ await this.#apiClient.cloud.shareDataInHistory.mutate({
41
46
  dataId: data.id,
42
47
  nodeId,
43
48
  users: shares,
@@ -45,180 +50,12 @@ export class SecrecyCloudClient {
45
50
  }
46
51
  return internalNodeFullToNodeFull(node);
47
52
  }
48
- async uploadData({ storageType, data, encrypted = true, encryptProgress, uploadProgress, signal, meta, }) {
49
- const dataBuffer = data instanceof File ? new Uint8Array(await data.arrayBuffer()) : data;
50
- let filetype;
51
- if (meta === true) {
52
- filetype = await fileTypeFromBuffer(dataBuffer);
53
- }
54
- else if (typeof meta !== 'undefined') {
55
- filetype = meta;
56
- }
57
- else if (!encrypted) {
58
- filetype = await fileTypeFromBuffer(dataBuffer);
59
- }
60
- if (storageType === 'lite' && dataBuffer.byteLength > kiloToBytes(1024)) {
61
- throw new Error('The data is too big for lite upload!');
62
- }
63
- const compressed = encrypted ? compress(dataBuffer) : dataBuffer;
64
- const dataKey = encrypted ? secretStreamKeygen() : null;
65
- const { data: encryptedData, md5: md5Data, md5Encrypted, } = dataKey
66
- ? await encrypt(dataKey, compressed, encryptProgress, signal)
67
- : { data: compressed, md5: await md5(compressed) };
68
- const encryptedDataKey = dataKey
69
- ? encryptCryptoBox(dataKey, this.#client.uaIdentity.identityPubKey, this.#client.uaPrivateKey)
70
- : null;
71
- await uploadProgress?.({
72
- total: encryptedData.byteLength,
73
- current: 0,
74
- percent: 0,
53
+ async uploadData(opts) {
54
+ return uploadData({
55
+ ...opts,
56
+ keyPair: this.#keys,
57
+ apiClient: this.#apiClient,
75
58
  });
76
- if (storageType === 'lite') {
77
- const uploadDataArgs = encryptedDataKey && md5Encrypted
78
- ? {
79
- type: 'encrypted',
80
- content: Buffer.from(encryptedData),
81
- sizeEncrypted: BigInt(encryptedData.byteLength),
82
- size: BigInt(dataBuffer.byteLength),
83
- key: sodium.to_hex(encryptedDataKey),
84
- md5Encrypted,
85
- md5: md5Data,
86
- ...filetype,
87
- }
88
- : {
89
- type: 'unencrypted',
90
- content: Buffer.from(encryptedData),
91
- md5: md5Data,
92
- sizeEncrypted: undefined,
93
- size: BigInt(dataBuffer.byteLength),
94
- ...filetype,
95
- };
96
- const uploadData = await this.#client.apiClient.cloud.uploadLiteData.mutate(uploadDataArgs, { signal });
97
- await uploadProgress?.({
98
- total: encryptedData.byteLength,
99
- current: encryptedData.byteLength,
100
- percent: 1,
101
- });
102
- const localData = {
103
- id: uploadData.id,
104
- storageType: 'lite',
105
- size: uploadDataArgs.size,
106
- sizeEncrypted: uploadDataArgs.sizeEncrypted ?? null,
107
- data: dataBuffer,
108
- ...filetype,
109
- };
110
- dataContentCache.set(uploadData.id, localData);
111
- return localData;
112
- }
113
- if (storageType === 's3' || storageType === 'cold') {
114
- const uploadDataArgs = encryptedDataKey && md5Encrypted
115
- ? {
116
- type: 'encrypted',
117
- sizeEncrypted: BigInt(encryptedData.byteLength),
118
- size: BigInt(dataBuffer.byteLength),
119
- key: sodium.to_hex(encryptedDataKey),
120
- md5Encrypted,
121
- md5: md5Data,
122
- ...filetype,
123
- }
124
- : {
125
- type: 'unencrypted',
126
- md5: md5Data,
127
- size: BigInt(dataBuffer.byteLength),
128
- sizeEncrypted: undefined,
129
- ...filetype,
130
- };
131
- const uploadDataCaller = storageType === 's3'
132
- ? this.#client.apiClient.cloud.uploadData
133
- : this.#client.apiClient.cloud.uploadColdData;
134
- const uploadData = await uploadDataCaller.mutate(uploadDataArgs, {
135
- signal,
136
- });
137
- if (uploadData.parts.length === 0) {
138
- if (uploadData.type === 'authed' &&
139
- uploadData.keyPair.pub !== this.#client.uaIdentity.identityPubKey) {
140
- throw new Error('The public key does not match with cached key!');
141
- }
142
- await uploadProgress?.({
143
- total: encryptedData.byteLength,
144
- current: encryptedData.byteLength,
145
- percent: 1,
146
- });
147
- const localData = {
148
- id: uploadData.id,
149
- storageType: storageType,
150
- size: uploadDataArgs.size,
151
- sizeEncrypted: uploadDataArgs.sizeEncrypted ?? null,
152
- data: dataBuffer,
153
- ...filetype,
154
- };
155
- dataContentCache.set(uploadData.id, localData);
156
- return localData;
157
- }
158
- const uploadDataPartEnd = async (md5, order) => {
159
- return this.#client.apiClient.cloud.uploadDataPartEnd.mutate({ dataId: uploadData.id, md5, order }, { signal });
160
- };
161
- const chunkParts = new Array();
162
- for (const [index, chunk] of enumerate(chunks(encryptedData, Number(uploadData.partSize)))) {
163
- chunkParts.push({
164
- order: index + 1,
165
- data: chunk,
166
- md5: await md5(chunk),
167
- });
168
- }
169
- const progressParts = {};
170
- const onProgress = (part, progressEvent) => {
171
- progressParts[part] = progressEvent;
172
- const current = Object.values(progressParts).reduce((prv, cur) => prv + cur.transferredBytes, 0);
173
- void uploadProgress?.({
174
- percent: current / encryptedData.byteLength,
175
- total: encryptedData.byteLength,
176
- current,
177
- });
178
- };
179
- const byPart = async (part) => {
180
- const formData = new FormData();
181
- const chunk = chunkParts.find((p) => p.order === part.order);
182
- if (chunk === undefined) {
183
- return;
184
- }
185
- for (const [key, value] of Object.entries(part.fields)) {
186
- formData.append(key, value);
187
- }
188
- formData.append('file', new Blob([chunk.data], { type: filetype?.mime }), `${uploadData.id}-${chunk.order}`);
189
- // await ky.post(part.url, {
190
- // signal,
191
- // body: formData,
192
- // onUploadProgress: (progressEvent) => {
193
- // onProgress(part.order, progressEvent)
194
- // },
195
- // })
196
- await axios.post(part.url, formData, {
197
- onUploadProgress: (progressEvent) => {
198
- onProgress(part.order, {
199
- percent: progressEvent.progress ?? 0,
200
- totalBytes: progressEvent.total ?? 0,
201
- transferredBytes: progressEvent.loaded,
202
- });
203
- },
204
- });
205
- return uploadDataPartEnd(chunk.md5, chunk.order);
206
- };
207
- await promiseAllLimit(3, uploadData.parts.map((p) => async () => {
208
- await byPart(p);
209
- }));
210
- const localData = {
211
- id: uploadData.id,
212
- storageType: storageType,
213
- size: uploadDataArgs.size,
214
- sizeEncrypted: uploadDataArgs.sizeEncrypted ?? null,
215
- data: dataBuffer,
216
- ...filetype,
217
- };
218
- dataContentCache.set(uploadData.id, localData);
219
- return localData;
220
- }
221
- throw new Error(`The "${storageType}" is not implemented yet!`);
222
59
  }
223
60
  async uploadDataInCloud({ data, name, nodeId, encryptProgress, uploadProgress, storageType = 's3', signal, }) {
224
61
  const uploadedData = await this.uploadData({
@@ -231,21 +68,21 @@ export class SecrecyCloudClient {
231
68
  return await this.saveInCloud({ dataId: uploadedData.id, name, nodeId });
232
69
  }
233
70
  async deletedNodes() {
234
- const deletedNodes = await this.#client.apiClient.cloud.nodesDeleted.query({});
235
- return await Promise.all(deletedNodes.map(async (node) => await apiNodeToExternal(node, this.#client.keyPairs)));
71
+ const deletedNodes = await this.#apiClient.cloud.nodesDeleted.query({});
72
+ return await Promise.all(deletedNodes.map(async (node) => await apiNodeToExternal(node, this.#keys)));
236
73
  }
237
74
  async sharedNodes() {
238
- const nodesShared = await this.#client.apiClient.cloud.nodesShared.query({});
239
- return await Promise.all(nodesShared.map(async (node) => await apiNodeToExternal(node, this.#client.keyPairs)));
75
+ const nodesShared = await this.#apiClient.cloud.nodesShared.query();
76
+ return await Promise.all(nodesShared.map(async (node) => await apiNodeToExternal(node, this.#keys)));
240
77
  }
241
78
  async nodesSharedWithMe(type = 'FOLDER') {
242
- const nodesSharedWithMe = await this.#client.apiClient.cloud.nodesSharedWithMe.query({ type });
243
- return await Promise.all(nodesSharedWithMe.map(async (node) => await apiNodeToExternal(node, this.#client.keyPairs)));
79
+ const nodesSharedWithMe = await this.#apiClient.cloud.nodesSharedWithMe.query({ type });
80
+ return await Promise.all(nodesSharedWithMe.map(async (node) => await apiNodeToExternal(node, this.#keys)));
244
81
  }
245
- async deleteNodeSharing({ nodeId, destPubKey, }) {
246
- const { isDeleted } = await this.#client.apiClient.cloud.deleteNodeSharing.mutate({
82
+ async deleteNodeSharing({ nodeId, userId, }) {
83
+ const { isDeleted } = await this.#apiClient.cloud.deleteNodeSharing.mutate({
247
84
  nodeId,
248
- destPubKey,
85
+ userId,
249
86
  });
250
87
  return isDeleted;
251
88
  }
@@ -258,14 +95,14 @@ export class SecrecyCloudClient {
258
95
  throw new Error(`Node (${nodeId}) does not exists`);
259
96
  }
260
97
  }
261
- if (!node.accesses[0]) {
262
- throw new Error(`No access found for node ${nodeId}`);
98
+ if (node.access?.nameKey === undefined) {
99
+ throw new Error(`Can't have access to node ${nodeId}`);
263
100
  }
264
101
  name =
265
- typeof name === 'string' && node.accesses[0].nameKey !== null
266
- ? await encryptName(name, node.accesses[0].nameKey)
102
+ typeof name === 'string' && node.access.nameKey !== null
103
+ ? await encryptName(name, node.access.nameKey)
267
104
  : null;
268
- const { isDuplicated } = await this.#client.apiClient.cloud.duplicateNode.mutate({
105
+ const { isDuplicated } = await this.#apiClient.cloud.duplicateNode.mutate({
269
106
  nodeId,
270
107
  folderId: folderId ?? null,
271
108
  name,
@@ -273,54 +110,50 @@ export class SecrecyCloudClient {
273
110
  return isDuplicated;
274
111
  }
275
112
  async deleteNodeCloudTrash({ ids }) {
276
- const { isDeleted } = await this.#client.apiClient.cloud.deleteNodeCloudTrash.mutate({ ids });
113
+ const { isDeleted } = await this.#apiClient.cloud.deleteNodeCloudTrash.mutate({ ids });
277
114
  return isDeleted;
278
115
  }
279
116
  async createFolder({ name, parentFolderId, }) {
280
117
  const { encryptedNameKey, encryptedName } = await generateAndEncryptNameAndKey({
281
118
  name,
282
- privateKey: this.#client.uaPrivateKey,
283
- publicKey: this.#client.uaIdentity.identityPubKey,
119
+ privateKey: this.#keys.privateKey,
120
+ publicKey: this.#keys.publicKey,
284
121
  });
285
- const createdFolder = await this.#client.apiClient.cloud.createFolder.mutate({
122
+ const createdFolder = await this.#apiClient.cloud.createFolder.mutate({
286
123
  name: encryptedName,
287
124
  parentId: parentFolderId ?? null,
288
125
  nameKey: encryptedNameKey,
289
126
  });
290
- const folder = await apiNodeToExternalNodeFull(createdFolder, this.#client.keyPairs);
291
- if (folder.parent) {
292
- const othersIdentities = Object.entries(folder.parent.identities).filter(([id]) => id !== this.#client.uaIdentity.identityPubKey);
293
- // TODO: ??
294
- if (othersIdentities.length > 0) {
295
- await this.shareNode(othersIdentities.map(([identityPubKey, permissions]) => ({
296
- pubKey: identityPubKey,
297
- nodeId: folder.id,
298
- rights: permissions.rights,
299
- addAccess: permissions.addAccess,
300
- delAccess: permissions.delAccess,
301
- sharingAddAccess: permissions.sharingAddAccess,
302
- sharingDelAccess: permissions.sharingDelAccess,
303
- })));
304
- }
127
+ const folder = await apiNodeToExternalNodeFull(createdFolder, this.#keys);
128
+ const users = folder.parent?.users?.filter(([u]) => u.id !== folder.owner.id) ?? [];
129
+ if (users.length > 0) {
130
+ await this.shareNode(users.map(([user, permissions]) => ({
131
+ userId: user.id,
132
+ nodeId: folder.id,
133
+ rights: permissions.rights,
134
+ addAccess: permissions.addAccess,
135
+ delAccess: permissions.delAccess,
136
+ sharingAddAccess: permissions.sharingAddAccess,
137
+ sharingDelAccess: permissions.sharingDelAccess,
138
+ })));
305
139
  }
306
140
  return folder;
307
141
  }
308
142
  async node({ id, deleted, } = {}) {
309
- const node = await this.#client.apiClient.cloud.nodeFullById.query({
310
- id,
311
- deleted,
312
- });
313
- return await apiNodeToExternalNodeFull(node, this.#client.keyPairs);
143
+ const node = await this.#apiClient.cloud.nodeFullById.query({ id, deleted });
144
+ return await apiNodeToExternalNodeFull(node, this.#keys);
314
145
  }
315
146
  async dataMetadata({ id }) {
316
- const data = await this.#client.apiClient.cloud.dataById.query({ id });
317
- return apiDataToExternal(data, this.#client.keyPairs);
147
+ const data = await this.#apiClient.cloud.dataById.query({ id });
148
+ return apiDataToExternal(data, this.#keys);
318
149
  }
319
- async shareNode(accesses, progress) {
150
+ async shareNode(input, progress) {
320
151
  // TODO: Validate input
321
- const nodesMap = await this.#client.apiClient.cloud.shareNode.mutate({
322
- accesses,
323
- });
152
+ const nodesMap = await this.#apiClient.cloud.shareNode.mutate(input);
153
+ const neededUserKey = Object.entries(nodesMap)
154
+ .filter(([, nodes]) => nodes.some((node) => node.includeKeys))
155
+ .map(([userId]) => userId);
156
+ const publicKeysMap = await this.#client.app.userPublicKey(neededUserKey);
324
157
  const maxNodesBatchSize = 1000;
325
158
  const totalNodesToShare = Object.values(nodesMap).reduce((size, ids) => size + ids.length, 0);
326
159
  progress?.({ total: totalNodesToShare, current: 0, percent: 0 });
@@ -337,9 +170,9 @@ export class SecrecyCloudClient {
337
170
  return filtered.length > 0 ? [userId, filtered] : null;
338
171
  })
339
172
  .filter((entry) => entry !== null));
340
- const infos = await this.encryptNodesForIdentities(nodesToEncrypt);
341
- const nodesToUpdateRights = Object.fromEntries(Object.entries(nodesMap).map(([pubKey, nodes]) => [
342
- pubKey,
173
+ const infos = await this.encryptNodesForUsers(nodesToEncrypt, publicKeysMap);
174
+ const nodesToUpdateRights = Object.fromEntries(Object.entries(nodesMap).map(([userId, nodes]) => [
175
+ userId,
343
176
  nodes
344
177
  .filter((node) => !node.includeKeys)
345
178
  .map((node) => {
@@ -355,13 +188,13 @@ export class SecrecyCloudClient {
355
188
  };
356
189
  }),
357
190
  ]));
358
- const withKeys = Object.fromEntries(Object.entries(infos).map(([pubKey, nodes]) => {
191
+ const withKeys = Object.fromEntries(Object.entries(infos).map(([userId, nodes]) => {
359
192
  return [
360
- pubKey,
193
+ userId,
361
194
  {
362
- pubKey,
195
+ userId,
363
196
  nodes: nodes.map((node) => {
364
- const map = nodesMap[pubKey];
197
+ const map = nodesMap[userId];
365
198
  if (!map) {
366
199
  throw new Error('Unable to retrieve mapped nodes!');
367
200
  }
@@ -384,22 +217,20 @@ export class SecrecyCloudClient {
384
217
  ];
385
218
  }));
386
219
  const finishInput = [];
387
- for (const [pubKey, nodes] of Object.entries(withKeys)) {
220
+ for (const [userId, nodes] of Object.entries(withKeys)) {
388
221
  if (nodes.nodes.length === 0) {
389
222
  continue;
390
223
  }
391
- finishInput.push({ pubKey, nodes: nodes.nodes });
224
+ finishInput.push({ userId, nodes: nodes.nodes });
392
225
  }
393
- for (const [pubKey, nodes] of Object.entries(nodesToUpdateRights)) {
226
+ for (const [userId, nodes] of Object.entries(nodesToUpdateRights)) {
394
227
  if (nodes.length === 0) {
395
228
  continue;
396
229
  }
397
- finishInput.push({ pubKey, nodes });
230
+ finishInput.push({ userId, nodes });
398
231
  }
399
232
  const subState = finishInput.length > 0
400
- ? await this.#client.apiClient.cloud.shareNodeFinish.mutate({
401
- accesses: finishInput,
402
- })
233
+ ? await this.#apiClient.cloud.shareNodeFinish.mutate(finishInput)
403
234
  : { isFinished: true, details: {} };
404
235
  const currentProgress = Math.min((index + 1) * maxNodesBatchSize, totalNodesToShare);
405
236
  progress?.({
@@ -446,31 +277,27 @@ export class SecrecyCloudClient {
446
277
  throw `Can't find Node ${nodeId}`;
447
278
  }
448
279
  }
449
- if (node.accesses.find((a) => ['delete', 'write'].includes(a.rights)) ===
450
- undefined) {
451
- throw new Error(`No access to update node ${nodeId}`);
452
- }
453
- if (!node.accesses[0]) {
454
- throw new Error(`No access found for node ${nodeId}`);
280
+ if (node.access?.nameKey === undefined) {
281
+ throw new Error(`Can't have access to node ${nodeId}`);
455
282
  }
456
283
  name =
457
- typeof name === 'string' && node.accesses[0].nameKey !== null
458
- ? await encryptName(name, node.accesses[0].nameKey)
284
+ typeof name === 'string' && node.access.nameKey !== null
285
+ ? await encryptName(name, node.access.nameKey)
459
286
  : null;
460
- const updateNode = await this.#client.apiClient.cloud.updateNode.mutate({
287
+ const updateNode = await this.#apiClient.cloud.updateNode.mutate({
461
288
  deletedAt: deletedAt ?? null,
462
289
  id: nodeId,
463
290
  isFavorite: isFavorite ?? null,
464
291
  name,
465
292
  });
466
- return await apiNodeToExternalNodeFull(updateNode, this.#client.keyPairs);
293
+ return await apiNodeToExternalNodeFull(updateNode, this.#keys);
467
294
  }
468
295
  async dataContent({ dataId, onDownloadProgress, progressDecrypt, signal, }) {
469
296
  const cached = dataContentCache.get(dataId);
470
297
  if (cached !== undefined) {
471
298
  return cached;
472
299
  }
473
- const dataContent = await this.#client.apiClient.cloud.dataContentById.query({
300
+ const dataContent = await this.#apiClient.cloud.dataContentById.query({
474
301
  id: dataId,
475
302
  });
476
303
  const totalBytes = Number(dataContent.sizeEncrypted ?? dataContent.size);
@@ -491,7 +318,7 @@ export class SecrecyCloudClient {
491
318
  if (cachedData.length === dataIds.length) {
492
319
  return cachedData;
493
320
  }
494
- const missingContents = await this.#client.apiClient.cloud.dataContentByIds.query({
321
+ const missingContents = await this.#apiClient.cloud.dataContentByIds.query({
495
322
  ids: dataIds.filter((dataId) => !cachedData.some((datum) => datum.id === dataId)),
496
323
  });
497
324
  const allDataContents = [
@@ -522,33 +349,33 @@ export class SecrecyCloudClient {
522
349
  }));
523
350
  }
524
351
  async deleteNodes({ nodeIds, }) {
525
- return this.#client.apiClient.cloud.deleteNodes.mutate({ ids: nodeIds });
352
+ return this.#apiClient.cloud.deleteNodes.mutate({ ids: nodeIds });
526
353
  }
527
354
  async deleteData({ dataId, nodeId, }) {
528
- const { isDeleted } = await this.#client.apiClient.cloud.deleteData.mutate({
355
+ const { isDeleted } = await this.#apiClient.cloud.deleteData.mutate({
529
356
  dataId,
530
357
  nodeId,
531
358
  });
532
359
  return isDeleted;
533
360
  }
534
361
  async deleteNode({ nodeId }) {
535
- const { isDeleted } = await this.#client.apiClient.cloud.deleteNode.mutate({
362
+ const { isDeleted } = await this.#apiClient.cloud.deleteNode.mutate({
536
363
  id: nodeId,
537
364
  });
538
365
  return isDeleted;
539
366
  }
540
367
  async emptyTrash() {
541
- const { isCleaned } = await this.#client.apiClient.cloud.emptyNodeCloudTrash.mutate({});
368
+ const { isCleaned } = await this.#apiClient.cloud.emptyNodeCloudTrash.mutate({});
542
369
  return isCleaned;
543
370
  }
544
371
  async recoverNode(id) {
545
- const { isRecovered } = await this.#client.apiClient.cloud.recoverNode.mutate({
372
+ const { isRecovered } = await this.#apiClient.cloud.recoverNode.mutate({
546
373
  id,
547
374
  });
548
375
  return isRecovered;
549
376
  }
550
377
  async moveNodes({ nodeIds, parentNodeId, }) {
551
- const { isMoved } = await this.#client.apiClient.cloud.moveNodes.mutate({
378
+ const { isMoved } = await this.#apiClient.cloud.moveNodes.mutate({
552
379
  ids: nodeIds,
553
380
  parentId: parentNodeId ?? null,
554
381
  });
@@ -593,7 +420,7 @@ export class SecrecyCloudClient {
593
420
  }
594
421
  const senderPubKey = await this.#client.app.userPublicKey(mail.sender.id);
595
422
  const dataKey = attachment.key
596
- ? decryptCryptoBox(sodium.from_hex(attachment.key), senderPubKey, this.#client.uaPrivateKey)
423
+ ? decryptCryptoBox(sodium.from_hex(attachment.key), senderPubKey, this.#keys.privateKey)
597
424
  : null;
598
425
  key = dataKey !== null ? sodium.to_hex(dataKey) : null;
599
426
  }
@@ -605,27 +432,28 @@ export class SecrecyCloudClient {
605
432
  key = data.key;
606
433
  }
607
434
  key = key
608
- ? sodium.to_hex(encryptCryptoBox(sodium.from_hex(key), this.#client.uaIdentity.identityPubKey, this.#client.uaPrivateKey))
435
+ ? sodium.to_hex(encryptCryptoBox(sodium.from_hex(key), this.#keys.publicKey, this.#keys.privateKey))
609
436
  : null;
610
437
  const { encryptedNameKey, encryptedName } = await generateAndEncryptNameAndKey({
611
438
  name,
612
- privateKey: this.#client.uaPrivateKey,
613
- publicKey: this.#client.uaIdentity.identityPubKey,
439
+ privateKey: this.#keys.privateKey,
440
+ publicKey: this.#keys.publicKey,
614
441
  });
615
- const saveInCloud = await this.#client.apiClient.cloud.saveInCloud.mutate({
442
+ const saveInCloud = await this.#apiClient.cloud.saveInCloud.mutate({
616
443
  dataId,
617
444
  key,
618
445
  nodeId: nodeId ?? null,
619
446
  fileName: encryptedName,
620
447
  nameKey: encryptedNameKey,
621
448
  });
622
- const node = await apiNodeToExternalNodeFull(saveInCloud, this.#client.keyPairs);
623
- if (node.parent) {
624
- const othersIdentities = Object.entries(node.parent.identities).filter(([id]) => id !== this.#client.uaIdentity.identityPubKey);
625
- // TODO: ??
626
- if (othersIdentities.length > 0) {
627
- await this.shareNode(othersIdentities.map(([identityPubKey, permissions]) => ({
628
- pubKey: identityPubKey,
449
+ const node = await apiNodeToExternalNodeFull(saveInCloud, this.#keys);
450
+ const me = node.parent?.users.find(([u]) => u.id === node.owner.id);
451
+ // TODO: ??
452
+ if (me !== undefined && ['delete', 'write'].includes(me[1].rights)) {
453
+ const others = node.parent?.users.filter(([u]) => u.id !== node.owner.id) ?? [];
454
+ if (others.length > 0) {
455
+ await this.shareNode(others.map(([user, permissions]) => ({
456
+ userId: user.id,
629
457
  nodeId: node.id,
630
458
  ...permissions,
631
459
  })));
@@ -633,13 +461,20 @@ export class SecrecyCloudClient {
633
461
  }
634
462
  return node;
635
463
  }
636
- encryptNodesForIdentities = async (identityNodes) => {
637
- const pubKeys = Object.keys(identityNodes).map((identityPubKey) => identityPubKey);
638
- const nodeIds = Object.values(identityNodes).flatMap((nodeIds) => nodeIds);
464
+ encryptNodesForUsers = async (userNodes, // { [userId]: nodeIds[] }
465
+ userPublicKeys) => {
466
+ const userIds = Object.keys(userNodes).map((userId) => userId);
467
+ const nodeIds = Object.values(userNodes).flatMap((nodeIds) => nodeIds);
639
468
  if (nodeIds.length === 0) {
640
469
  return {};
641
470
  }
642
471
  const nodes = [];
472
+ // Pre check to ensure we get all public keys for users!
473
+ for (const userId of userIds) {
474
+ if (userPublicKeys[userId] === undefined) {
475
+ throw new Error(`Unable to retrieve some user public keys!`);
476
+ }
477
+ }
643
478
  // Retrieve and format nodes.
644
479
  for (const nodeId of nodeIds) {
645
480
  let node = nodesEncryptionCache.get(nodeId) ?? nodesCache.get(nodeId);
@@ -664,7 +499,7 @@ export class SecrecyCloudClient {
664
499
  // Retrieve all missing nodes from cache to api.
665
500
  const missingNodeIds = nodeIds.filter((nodeId) => !nodes.some((node) => node.id === nodeId));
666
501
  const fetchedNodes = missingNodeIds.length > 0
667
- ? await this.#client.apiClient.cloud.nodesForEncryption.query({
502
+ ? await this.#apiClient.cloud.nodesForEncryption.query({
668
503
  ids: missingNodeIds,
669
504
  })
670
505
  : [];
@@ -673,13 +508,13 @@ export class SecrecyCloudClient {
673
508
  throw new Error(`Unable to fetch some node infos (${diff.join(', ')})`);
674
509
  }
675
510
  const finalNodes = await Promise.all(fetchedNodes.map((node) => {
676
- return apiNodeForEncryptionToInternal(node, this.#client.keyPairs);
511
+ return apiNodeForEncryptionToInternal(node, this.#keys);
677
512
  }));
678
513
  nodes.push(...finalNodes);
679
514
  const nodesMappedUsers = {};
680
- for (const pubKey in identityNodes) {
681
- nodesMappedUsers[pubKey] ??= [];
682
- for (const nodeId of identityNodes[pubKey]) {
515
+ for (const userId in userNodes) {
516
+ nodesMappedUsers[userId] ??= [];
517
+ for (const nodeId of userNodes[userId]) {
683
518
  const node = nodes.find((node) => node.id === nodeId);
684
519
  if (!node) {
685
520
  throw new Error('Unable to retrieve node from ram');
@@ -689,16 +524,17 @@ export class SecrecyCloudClient {
689
524
  throw new Error(`Can't share a node without data (${node.id})!`);
690
525
  }
691
526
  const nameKey = node.access?.nameKey;
692
- nodesMappedUsers[pubKey].push({
527
+ const publicKey = userPublicKeys[userId];
528
+ nodesMappedUsers[userId].push({
693
529
  id: node.id,
694
530
  nameKey: nameKey !== null
695
- ? sodium.to_hex(encryptCryptoBox(sodium.from_hex(nameKey), pubKey, this.#client.uaPrivateKey))
531
+ ? sodium.to_hex(encryptCryptoBox(sodium.from_hex(nameKey), publicKey, this.#keys.privateKey))
696
532
  : null,
697
533
  data: 'history' in node
698
534
  ? node.history.map((f) => ({
699
535
  id: f.id,
700
536
  key: f.key
701
- ? sodium.to_hex(encryptCryptoBox(sodium.from_hex(f.key), pubKey, this.#client.uaPrivateKey))
537
+ ? sodium.to_hex(encryptCryptoBox(sodium.from_hex(f.key), publicKey, this.#keys.privateKey))
702
538
  : null,
703
539
  }))
704
540
  : [],
@@ -709,15 +545,15 @@ export class SecrecyCloudClient {
709
545
  };
710
546
  async reportData({ id, reasons, }) {
711
547
  const [rawData, secrecy] = await Promise.all([
712
- this.#client.apiClient.cloud.dataById.query({ id }),
713
- this.#client.apiClient.misc.publicKey.query(),
548
+ this.#apiClient.cloud.dataById.query({ id }),
549
+ this.#apiClient.misc.publicKey.query(),
714
550
  ]);
715
- const data = apiDataToInternal(rawData, this.#client.keyPairs);
716
- return this.#client.apiClient.cloud.reportData.mutate({
551
+ const data = apiDataToInternal(rawData, this.#keys);
552
+ return this.#apiClient.cloud.reportData.mutate({
717
553
  id: id,
718
554
  reasons: reasons,
719
555
  encryptedDataKey: data.key
720
- ? sodium.to_hex(encryptCryptoBox(sodium.from_hex(data.key), secrecy.publicKey, this.#client.uaPrivateKey))
556
+ ? sodium.to_hex(encryptCryptoBox(sodium.from_hex(data.key), secrecy.publicKey, this.#keys.privateKey))
721
557
  : null,
722
558
  });
723
559
  }
@@ -731,95 +567,47 @@ export class SecrecyCloudClient {
731
567
  throw new Error("It's not possible to transfer a cold stored data to a lite storage!");
732
568
  }
733
569
  }
734
- return this.#client.apiClient.cloud.moveToStorageType.mutate(input);
570
+ return this.#apiClient.cloud.moveToStorageType.mutate(input);
735
571
  }
736
572
  getPublicDataLink(input) {
737
- return this.#client.apiClient.cloud.dataLink.query(input);
573
+ return this.#apiClient.cloud.dataLink.query(input);
738
574
  }
739
575
  getPublicDataLinks(input) {
740
- return this.#client.apiClient.cloud.dataLinks.query(input);
576
+ return this.#apiClient.cloud.dataLinks.query(input);
741
577
  }
742
578
  checkAccesses(input) {
743
- return this.#client.apiClient.cloud.checkAccesses.query(input);
579
+ return this.#apiClient.cloud.checkAccesses.query(input);
744
580
  }
745
581
  createPublicDataLink(input) {
746
- if (input.expireAt && input.expireAt <= new Date()) {
747
- throw new Error('Unable to create public link using a past expireAt date!');
748
- }
749
- return this.#client.apiClient.cloud.createDataLink.mutate(input);
582
+ return createPublicDataLink({ ...input, apiClient: this.#apiClient });
750
583
  }
751
584
  updatePublicDataLink(input) {
752
- return this.#client.apiClient.cloud.updateDataLink.mutate(input);
585
+ return this.#apiClient.cloud.updateDataLink.mutate(input);
753
586
  }
754
587
  deletePublicDataLink(input) {
755
- return this.#client.apiClient.cloud.deleteDataLink.mutate(input);
588
+ return this.#apiClient.cloud.deleteDataLink.mutate(input);
589
+ }
590
+ downloadDataFromLink(input) {
591
+ return downloadDataFromLink({
592
+ ...input,
593
+ dataUrl: this.#client.secrecyUrls.data,
594
+ });
756
595
  }
757
596
  async _handleDataContent({ dataContent, totalBytes, progressParts, onDownloadProgress, progressDecrypt, signal, }) {
758
- const onProgress = (key, progressEvent) => {
759
- progressParts[key] = progressEvent;
760
- const transferredBytes = Object.values(progressParts).reduce((prv, cur) => prv + cur.transferredBytes, 0);
761
- onDownloadProgress?.({
762
- percent: transferredBytes / totalBytes,
763
- totalBytes,
764
- transferredBytes,
765
- });
766
- };
767
- const encryptedContentFromParts = async (arg) => {
768
- const parts = {};
769
- const byPart = async (part) => {
770
- const buf = new Uint8Array(await ky
771
- .get(part.contentUrl, {
772
- timeout: false,
773
- onDownloadProgress: (pr) => {
774
- onProgress(`${arg.dataId}-${part.order}`, pr);
775
- },
776
- signal,
777
- })
778
- .arrayBuffer());
779
- const md5Part = await md5(buf);
780
- if (md5Part !== part.md5) {
781
- throw new Error(`Invalid md5 for part ${part.order} of data ${arg.dataId}`);
782
- }
783
- if (typeof parts[arg.dataId] === 'undefined') {
784
- parts[arg.dataId] = [{ data: buf, order: part.order }];
785
- }
786
- else {
787
- parts[arg.dataId].push({ data: buf, order: part.order });
788
- }
789
- };
790
- await promiseAllLimit(3, arg.dataParts.map((p) => async () => byPart(p)));
791
- return concatenate(...parts[arg.dataId]
792
- .sort((a, b) => a.order - b.order)
793
- .map((p) => p.data));
794
- };
795
- const encryptedContent = dataContent.type === 'lite'
796
- ? new Uint8Array(dataContent.content)
797
- : dataContent.type === 'cloud'
798
- ? await encryptedContentFromParts({
799
- dataId: dataContent.id,
800
- dataParts: dataContent.parts,
801
- })
802
- : dataContent.maybeContent !== null
803
- ? new Uint8Array(dataContent.maybeContent)
804
- : dataContent.maybeParts !== null
805
- ? await encryptedContentFromParts({
806
- dataId: dataContent.id,
807
- dataParts: dataContent.maybeParts,
808
- })
809
- : null;
810
- if (encryptedContent === null) {
811
- throw `Can't find content for data ${dataContent.id}`;
812
- }
813
- const md5Encrypted = await md5(encryptedContent);
814
- if (md5Encrypted !== dataContent.md5Encrypted) {
815
- throw new Error(`Encrypted content does not match`);
816
- }
597
+ const { encryptedContent } = await buildBytesFromApiData({
598
+ dataContent,
599
+ totalBytes,
600
+ progressParts,
601
+ onDownloadProgress,
602
+ progressDecrypt,
603
+ signal,
604
+ });
817
605
  const key = dataContent.key
818
606
  ? decryptCryptoBox(sodium.from_hex(dataContent.key), dataContent.type === 'received_mail'
819
607
  ? dataContent.senderPublicKey
820
608
  : dataContent.type === 'cloud' || dataContent.type === 'lite'
821
609
  ? dataContent.publicKey
822
- : this.#client.uaIdentity.identityPubKey, this.#client.uaPrivateKey)
610
+ : this.#keys.publicKey, this.#keys.privateKey)
823
611
  : null;
824
612
  const src = key
825
613
  ? await decrypt(key, encryptedContent, progressDecrypt, signal)