@secrecy/lib 1.50.0 → 1.51.0-feat-improvements.2

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/dist/lib/cache.js CHANGED
@@ -8,6 +8,6 @@ export const dataContentCache = new LRUCache({
8
8
  max: 500,
9
9
  maxSize: gigaToBytes(0.5),
10
10
  sizeCalculation: (value) => {
11
- return value.byteLength;
11
+ return value.data.byteLength;
12
12
  },
13
13
  });
@@ -49,9 +49,9 @@ export class SecrecyCloudClient {
49
49
  }
50
50
  return internalNodeFullToNodeFull(node);
51
51
  }
52
- async uploadLiteData({ data, encrypted = true, encryptProgress, uploadProgress, signal, }) {
52
+ async uploadData({ storageType, data, encrypted = true, encryptProgress, uploadProgress, signal, }) {
53
53
  const dataBuffer = data instanceof File ? new Uint8Array(await data.arrayBuffer()) : data;
54
- if (dataBuffer.byteLength > kiloToBytes(4500)) {
54
+ if (storageType === 'lite' && dataBuffer.byteLength > kiloToBytes(4500)) {
55
55
  throw new Error('The data is too big for lite upload!');
56
56
  }
57
57
  const compressed = compress(dataBuffer);
@@ -70,137 +70,132 @@ export class SecrecyCloudClient {
70
70
  current: 0,
71
71
  percent: 0,
72
72
  });
73
- const uploadData = await this.#apiClient.cloud.uploadLiteData.mutate(encryptedDataKey && md5Encrypted
74
- ? {
75
- type: 'encrypted',
76
- content: Buffer.from(encryptedData),
77
- sizeEncrypted: BigInt(encryptedData.byteLength),
78
- size: BigInt(dataBuffer.byteLength),
79
- key: sodium.to_hex(encryptedDataKey),
80
- md5Encrypted,
81
- md5: md5Data,
82
- }
83
- : {
84
- type: 'unencrypted',
85
- content: Buffer.from(encryptedData),
86
- md5: md5Data,
87
- size: BigInt(dataBuffer.byteLength),
88
- }, { signal });
89
- await uploadProgress?.({
90
- total: encryptedData.byteLength,
91
- current: encryptedData.byteLength,
92
- percent: 1,
93
- });
94
- return uploadData.id;
95
- }
96
- async uploadData({ data, encrypted = true, encryptProgress, uploadProgress, signal, }) {
97
- const dataKey = encrypted ? secretStreamKeygen() : null;
98
- const dataBuffer = data instanceof File ? new Uint8Array(await data.arrayBuffer()) : data;
99
- const compressed = compress(dataBuffer);
100
- const { data: encryptedData, md5: md5Data, md5Encrypted, } = dataKey
101
- ? await encrypt(dataKey, compressed, encryptProgress, signal)
102
- : {
103
- data: compressed,
104
- md5: await md5(compressed),
105
- };
106
- const encryptedDataKey = dataKey
107
- ? encryptCryptoBox(dataKey, this.#keys.publicKey, this.#keys.privateKey)
108
- : null;
109
- const uploadData = await this.#apiClient.cloud.uploadData.mutate(encryptedDataKey && md5Encrypted
110
- ? {
111
- type: 'encrypted',
112
- sizeEncrypted: BigInt(encryptedData.byteLength),
113
- size: BigInt(dataBuffer.byteLength),
114
- key: sodium.to_hex(encryptedDataKey),
115
- md5Encrypted,
116
- md5: md5Data,
117
- }
118
- : {
119
- type: 'unencrypted',
120
- md5: md5Data,
121
- size: BigInt(dataBuffer.byteLength),
122
- }, { signal });
123
- await uploadProgress?.({
124
- total: encryptedData.byteLength,
125
- current: 0,
126
- percent: 0,
127
- });
128
- if (uploadData.parts.length === 0) {
129
- if (uploadData.keyPair.pub !== this.#keys.publicKey) {
130
- throw new Error('The public key does not match with cached key!');
131
- }
73
+ if (storageType === 'lite') {
74
+ const uploadData = await this.#apiClient.cloud.uploadLiteData.mutate(encryptedDataKey && md5Encrypted
75
+ ? {
76
+ type: 'encrypted',
77
+ content: Buffer.from(encryptedData),
78
+ sizeEncrypted: BigInt(encryptedData.byteLength),
79
+ size: BigInt(dataBuffer.byteLength),
80
+ key: sodium.to_hex(encryptedDataKey),
81
+ md5Encrypted,
82
+ md5: md5Data,
83
+ }
84
+ : {
85
+ type: 'unencrypted',
86
+ content: Buffer.from(encryptedData),
87
+ md5: md5Data,
88
+ size: BigInt(dataBuffer.byteLength),
89
+ }, { signal });
132
90
  await uploadProgress?.({
133
91
  total: encryptedData.byteLength,
134
92
  current: encryptedData.byteLength,
135
93
  percent: 1,
136
94
  });
137
- return uploadData.id;
138
- }
139
- const uploadDataPartEnd = async (md5, order) => {
140
- return this.#apiClient.cloud.uploadDataPartEnd.mutate({
141
- dataId: uploadData.id,
142
- md5,
143
- order,
144
- }, { signal });
145
- };
146
- const chunkParts = new Array();
147
- for (const [index, chunk] of enumerate(chunks(encryptedData, Number(uploadData.partSize)))) {
148
- chunkParts.push({
149
- order: index + 1,
150
- data: chunk,
151
- md5: await md5(chunk),
152
- });
95
+ return {
96
+ object: 'lite',
97
+ id: uploadData.id,
98
+ };
153
99
  }
154
- const progressParts = {};
155
- const onProgress = (part, progressEvent) => {
156
- progressParts[part] = progressEvent;
157
- const current = Object.values(progressParts).reduce((prv, cur) => prv + cur.loaded, 0);
158
- void uploadProgress?.({
159
- percent: current / encryptedData.byteLength,
160
- total: encryptedData.byteLength,
161
- current,
162
- });
163
- };
164
- const byPart = async (part) => {
165
- const formData = new FormData();
166
- const chunk = chunkParts.find((p) => p.order === part.order);
167
- if (chunk === undefined) {
168
- return;
100
+ if (storageType === 's3') {
101
+ const uploadDataArgs = encryptedDataKey && md5Encrypted
102
+ ? {
103
+ type: 'encrypted',
104
+ sizeEncrypted: BigInt(encryptedData.byteLength),
105
+ size: BigInt(dataBuffer.byteLength),
106
+ key: sodium.to_hex(encryptedDataKey),
107
+ md5Encrypted,
108
+ md5: md5Data,
109
+ }
110
+ : {
111
+ type: 'unencrypted',
112
+ md5: md5Data,
113
+ size: BigInt(dataBuffer.byteLength),
114
+ };
115
+ const uploadData = await this.#apiClient.cloud.uploadData.mutate(uploadDataArgs, { signal });
116
+ if (uploadData.parts.length === 0) {
117
+ if (uploadData.keyPair.pub !== this.#keys.publicKey) {
118
+ throw new Error('The public key does not match with cached key!');
119
+ }
120
+ await uploadProgress?.({
121
+ total: encryptedData.byteLength,
122
+ current: encryptedData.byteLength,
123
+ percent: 1,
124
+ });
125
+ return {
126
+ object: 's3',
127
+ id: uploadData.id,
128
+ };
169
129
  }
170
- for (const [key, value] of Object.entries(part.fields)) {
171
- formData.append(key, value);
130
+ const uploadDataPartEnd = async (md5, order) => {
131
+ return this.#apiClient.cloud.uploadDataPartEnd.mutate({
132
+ dataId: uploadData.id,
133
+ md5,
134
+ order,
135
+ }, { signal });
136
+ };
137
+ const chunkParts = new Array();
138
+ for (const [index, chunk] of enumerate(chunks(encryptedData, Number(uploadData.partSize)))) {
139
+ chunkParts.push({
140
+ order: index + 1,
141
+ data: chunk,
142
+ md5: await md5(chunk),
143
+ });
172
144
  }
173
- formData.append('file', new Blob([chunk.data]), `${uploadData.id}-${chunk.order}`);
174
- await axios.post(part.url, formData, {
175
- onUploadProgress: (progressEvent) => {
176
- onProgress(part.order, progressEvent);
177
- },
178
- signal,
145
+ const progressParts = {};
146
+ const onProgress = (part, progressEvent) => {
147
+ progressParts[part] = progressEvent;
148
+ const current = Object.values(progressParts).reduce((prv, cur) => prv + cur.loaded, 0);
149
+ void uploadProgress?.({
150
+ percent: current / encryptedData.byteLength,
151
+ total: encryptedData.byteLength,
152
+ current,
153
+ });
154
+ };
155
+ const byPart = async (part) => {
156
+ const formData = new FormData();
157
+ const chunk = chunkParts.find((p) => p.order === part.order);
158
+ if (chunk === undefined) {
159
+ return;
160
+ }
161
+ for (const [key, value] of Object.entries(part.fields)) {
162
+ formData.append(key, value);
163
+ }
164
+ formData.append('file', new Blob([chunk.data]), `${uploadData.id}-${chunk.order}`);
165
+ await axios.post(part.url, formData, {
166
+ onUploadProgress: (progressEvent) => {
167
+ onProgress(part.order, progressEvent);
168
+ },
169
+ signal,
170
+ });
171
+ return uploadDataPartEnd(chunk.md5, chunk.order);
172
+ };
173
+ await promiseAllLimit(3, uploadData.parts.map((p) => async () => {
174
+ await byPart(p);
175
+ }));
176
+ dataContentCache.set(uploadData.id, {
177
+ id: uploadData.id,
178
+ storageType: 's3',
179
+ size: uploadDataArgs.size,
180
+ data: dataBuffer,
179
181
  });
180
- return uploadDataPartEnd(chunk.md5, chunk.order);
181
- };
182
- await promiseAllLimit(3, uploadData.parts.map((p) => async () => {
183
- await byPart(p);
184
- }));
185
- dataContentCache.set(uploadData.id, dataBuffer);
186
- return uploadData.id;
182
+ return {
183
+ id: uploadData.id,
184
+ object: 's3',
185
+ };
186
+ }
187
+ throw new Error(`The "${storageType}" is not implemented yet!`);
187
188
  }
188
- async uploadDataInCloud({ data, name, nodeId, encryptProgress, uploadProgress, signal, isLite, }) {
189
- const dataId = isLite
190
- ? await this.uploadLiteData({
191
- data,
192
- encryptProgress,
193
- uploadProgress,
194
- signal,
195
- })
196
- : await this.uploadData({
197
- data,
198
- encryptProgress,
199
- uploadProgress,
200
- signal,
201
- });
189
+ async uploadDataInCloud({ data, name, nodeId, encryptProgress, uploadProgress, storageType = 's3', signal, }) {
190
+ const uploadedData = await this.uploadData({
191
+ storageType,
192
+ data,
193
+ encryptProgress,
194
+ uploadProgress,
195
+ signal,
196
+ });
202
197
  return await this.saveInCloud({
203
- dataId,
198
+ dataId: uploadedData.id,
204
199
  name,
205
200
  nodeId,
206
201
  });
@@ -415,7 +410,141 @@ export class SecrecyCloudClient {
415
410
  throw `Can't find content for data ${dataId}`;
416
411
  }
417
412
  const data = await finalize(encryptedContent);
418
- dataContentCache.set(dataId, data);
413
+ dataContentCache.set(dataId, {
414
+ id: dataId,
415
+ storageType: dataContent.storageType,
416
+ size: dataContent.totalSize,
417
+ data,
418
+ });
419
+ return {
420
+ id: dataId,
421
+ storageType: dataContent.storageType,
422
+ data,
423
+ };
424
+ }
425
+ async dataContents({ dataIds, onDownloadProgress, progressDecrypt, signal, }) {
426
+ const cachedData = dataIds
427
+ .map((dataId) => dataContentCache.get(dataId))
428
+ .filter((data) => typeof data !== 'undefined');
429
+ if (cachedData.length === dataIds.length) {
430
+ return cachedData;
431
+ }
432
+ const missingContents = await this.#apiClient.cloud.dataContentByIds.query({
433
+ ids: dataIds.filter((dataId) => !cachedData.some((datum) => datum.id === dataId)),
434
+ });
435
+ const allDataContents = [
436
+ ...missingContents.map((data) => ({
437
+ id: data.id,
438
+ size: data.totalSize,
439
+ storageType: data.storageType,
440
+ })),
441
+ ...cachedData.map((data) => ({
442
+ id: data.id,
443
+ size: data.size,
444
+ storageType: data.storageType,
445
+ })),
446
+ ];
447
+ const allDataContentsBytes = Number(allDataContents.reduce((curr, next) => curr + next.size, 0n));
448
+ const progressParts = {};
449
+ const onProgress = (dataId, part, progressEvent) => {
450
+ progressParts[`${dataId}-${part}`] = progressEvent;
451
+ const transferredBytes = Object.values(progressParts).reduce((prv, cur) => prv + cur.transferredBytes, 0);
452
+ onDownloadProgress?.({
453
+ percent: transferredBytes / allDataContentsBytes,
454
+ totalBytes: allDataContentsBytes,
455
+ transferredBytes,
456
+ });
457
+ };
458
+ const encryptedContentFromParts = async ({ dataId, dataParts, }) => {
459
+ const parts = {};
460
+ const byPart = async (part) => {
461
+ const buf = new Uint8Array(await ky
462
+ .get(part.contentUrl, {
463
+ timeout: false,
464
+ onDownloadProgress: (pr) => {
465
+ onProgress(dataId, part.order, pr);
466
+ },
467
+ signal,
468
+ })
469
+ .arrayBuffer());
470
+ const md5Part = await md5(buf);
471
+ if (md5Part !== part.md5) {
472
+ throw new Error(`Invalid md5 for part ${part.order} of data ${dataId}`);
473
+ }
474
+ if (typeof parts[dataId] === 'undefined') {
475
+ parts[dataId] = [
476
+ {
477
+ data: buf,
478
+ order: part.order,
479
+ },
480
+ ];
481
+ }
482
+ else {
483
+ parts[dataId].push({
484
+ data: buf,
485
+ order: part.order,
486
+ });
487
+ }
488
+ };
489
+ await promiseAllLimit(3, dataParts.map((p) => async () => byPart(p)));
490
+ return concatenate(...parts[dataId].sort((a, b) => a.order - b.order).map((p) => p.data));
491
+ };
492
+ const finalize = async (dataContent, encryptedContent) => {
493
+ // const md5Encrypted = await firstValueFrom(md5(of(encryptedContent)));
494
+ const md5Encrypted = await md5(encryptedContent);
495
+ if (md5Encrypted !== dataContent.md5Encrypted) {
496
+ throw new Error(`Encrypted content does not match`);
497
+ }
498
+ const key = dataContent.key
499
+ ? decryptCryptoBox(sodium.from_hex(dataContent.key), dataContent.type === 'received_mail'
500
+ ? dataContent.senderPublicKey
501
+ : dataContent.type === 'cloud'
502
+ ? dataContent.publicKey
503
+ : this.#keys.publicKey, this.#keys.privateKey)
504
+ : null;
505
+ const src = key
506
+ ? await decrypt(key, encryptedContent, progressDecrypt, signal)
507
+ : encryptedContent;
508
+ // const md5Content = await firstValueFrom(md5(of(src)));
509
+ const md5Content = await md5(src);
510
+ if (md5Content !== dataContent.md5) {
511
+ throw new Error(`Content does not match`);
512
+ }
513
+ return decompress(src);
514
+ };
515
+ const encrypt = async (dataContent) => {
516
+ const encryptedContent = dataContent.type === 'lite'
517
+ ? new Uint8Array(dataContent.content)
518
+ : dataContent.type === 'cloud'
519
+ ? await encryptedContentFromParts({
520
+ dataId: dataContent.id,
521
+ dataParts: dataContent.parts,
522
+ })
523
+ : dataContent.maybeContent !== null
524
+ ? new Uint8Array(dataContent.maybeContent)
525
+ : dataContent.maybeParts !== null
526
+ ? await encryptedContentFromParts({
527
+ dataId: dataContent.id,
528
+ dataParts: dataContent.maybeParts,
529
+ })
530
+ : null;
531
+ if (encryptedContent === null) {
532
+ throw `Can't find content for data ${dataContent.id}`;
533
+ }
534
+ return encryptedContent;
535
+ };
536
+ const data = await Promise.all(missingContents.map(async (missingContent) => {
537
+ const encrypted = await encrypt(missingContent);
538
+ const finalized = await finalize(missingContent, encrypted);
539
+ const datum = {
540
+ id: missingContent.id,
541
+ size: missingContent.totalSize,
542
+ storageType: missingContent.storageType,
543
+ data: finalized,
544
+ };
545
+ dataContentCache.set(missingContent.id, datum);
546
+ return datum;
547
+ }));
419
548
  return data;
420
549
  }
421
550
  async deleteData({ dataId, nodeId, }) {
@@ -431,11 +560,6 @@ export class SecrecyCloudClient {
431
560
  });
432
561
  return isDeleted;
433
562
  }
434
- async deleteNodes({ nodeIds, }) {
435
- return this.#apiClient.cloud.deleteNodes.mutate({
436
- ids: nodeIds,
437
- });
438
- }
439
563
  async emptyTrash() {
440
564
  const { isCleaned } = await this.#apiClient.cloud.emptyNodeCloudTrash.mutate({});
441
565
  return isCleaned;
@@ -1,4 +1,4 @@
1
- import type { InternalNode, InternalData, InternalNodeFull } from './client/types/index.js';
1
+ import type { InternalNode, InternalData, InternalNodeFull, DataStorageType } from './client/types/index.js';
2
2
  import { LRUCache } from 'lru-cache';
3
3
  export declare const dataCache: Map<string, InternalData>;
4
4
  export declare const nodesCache: Map<string, InternalNode | InternalNodeFull>;
@@ -10,4 +10,9 @@ export declare const usersCache: Map<string, {
10
10
  isSearchable: boolean;
11
11
  }>;
12
12
  export declare const publicKeysCache: Map<string, string>;
13
- export declare const dataContentCache: LRUCache<string, Uint8Array, unknown>;
13
+ export declare const dataContentCache: LRUCache<string, {
14
+ id: string;
15
+ storageType: DataStorageType;
16
+ size: bigint;
17
+ data: Uint8Array;
18
+ }, unknown>;
@@ -1,5 +1,5 @@
1
1
  import type { ProgressCallback, SecrecyClient } from '../index.js';
2
- import type { DataMetadata, KeyPair, Node, NodeFull, NodeType, Rights } from './types/index.js';
2
+ import type { DataMetadata, DataStorageType, KeyPair, Node, NodeFull, NodeType, Rights } from './types/index.js';
3
3
  import { type RouterInputs, type ApiClient, type RouterOutputs } from '../client.js';
4
4
  import { type DownloadProgress } from '../types.js';
5
5
  export declare class SecrecyCloudClient {
@@ -9,28 +9,25 @@ export declare class SecrecyCloudClient {
9
9
  dataId: string;
10
10
  nodeId: string;
11
11
  }): Promise<NodeFull>;
12
- uploadLiteData({ data, encrypted, encryptProgress, uploadProgress, signal, }: {
12
+ uploadData({ storageType, data, encrypted, encryptProgress, uploadProgress, signal, }: {
13
+ storageType: DataStorageType;
13
14
  data: globalThis.File | Uint8Array;
14
15
  encrypted?: boolean;
15
16
  encryptProgress?: ProgressCallback;
16
17
  uploadProgress?: ProgressCallback;
17
18
  signal?: AbortSignal;
18
- }): Promise<string>;
19
- uploadData({ data, encrypted, encryptProgress, uploadProgress, signal, }: {
20
- data: globalThis.File | Uint8Array;
21
- encrypted?: boolean;
22
- encryptProgress?: ProgressCallback;
23
- uploadProgress?: ProgressCallback;
24
- signal?: AbortSignal;
25
- }): Promise<string>;
26
- uploadDataInCloud({ data, name, nodeId, encryptProgress, uploadProgress, signal, isLite, }: {
19
+ }): Promise<{
20
+ id: string;
21
+ object: DataStorageType;
22
+ }>;
23
+ uploadDataInCloud({ data, name, nodeId, encryptProgress, uploadProgress, storageType, signal, }: {
27
24
  data: globalThis.File | Uint8Array;
28
25
  name: string;
29
26
  nodeId?: string;
30
27
  encryptProgress?: ProgressCallback;
31
28
  uploadProgress?: ProgressCallback;
32
29
  signal?: AbortSignal;
33
- isLite?: boolean;
30
+ storageType?: DataStorageType;
34
31
  }): Promise<NodeFull>;
35
32
  deletedNodes(): Promise<Node[]>;
36
33
  sharedNodes(): Promise<Node[]>;
@@ -74,7 +71,21 @@ export declare class SecrecyCloudClient {
74
71
  onDownloadProgress?: (progress: DownloadProgress) => void;
75
72
  progressDecrypt?: ProgressCallback;
76
73
  signal?: AbortSignal;
77
- }): Promise<Uint8Array>;
74
+ }): Promise<{
75
+ id: string;
76
+ storageType: DataStorageType;
77
+ data: Uint8Array;
78
+ }>;
79
+ dataContents({ dataIds, onDownloadProgress, progressDecrypt, signal, }: {
80
+ dataIds: string[];
81
+ onDownloadProgress?: (progress: DownloadProgress) => void;
82
+ progressDecrypt?: ProgressCallback;
83
+ signal?: AbortSignal;
84
+ }): Promise<{
85
+ id: string;
86
+ storageType: DataStorageType;
87
+ data: Uint8Array;
88
+ }[]>;
78
89
  deleteData({ dataId, nodeId, }: {
79
90
  dataId: string;
80
91
  nodeId: string;
@@ -82,11 +93,6 @@ export declare class SecrecyCloudClient {
82
93
  deleteNode({ nodeId }: {
83
94
  nodeId: string;
84
95
  }): Promise<boolean>;
85
- deleteNodes({ nodeIds, }: {
86
- nodeIds: string[];
87
- }): Promise<{
88
- count: number;
89
- }>;
90
96
  emptyTrash(): Promise<boolean>;
91
97
  recoverNode(id: string): Promise<boolean>;
92
98
  moveNodes({ nodeIds, parentNodeId, }: {
@@ -1,6 +1,8 @@
1
1
  import { type RouterOutputs } from '../../client.js';
2
+ export type ApiData = NonNullable<RouterOutputs['cloud']['dataById']>;
3
+ export type ApiExtendedData = NonNullable<RouterOutputs['cloud']['dataContentByIds'][number]>;
4
+ export type DataStorageType = ApiData['storageType'];
2
5
  export type DataMetadata = Pick<ApiData, 'id' | 'size' | 'sizeEncrypted' | 'md5' | 'md5Encrypted' | 'createdAt'>;
3
6
  export type InternalData = DataMetadata & {
4
7
  key: string | null;
5
8
  };
6
- export type ApiData = NonNullable<RouterOutputs['cloud']['dataById']>;