@protontech/drive-sdk 0.7.3 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/crypto/driveCrypto.js +1 -1
  2. package/dist/crypto/driveCrypto.js.map +1 -1
  3. package/dist/crypto/interface.d.ts +3 -1
  4. package/dist/crypto/openPGPCrypto.d.ts +4 -1
  5. package/dist/crypto/openPGPCrypto.js +2 -1
  6. package/dist/crypto/openPGPCrypto.js.map +1 -1
  7. package/dist/errors.d.ts +1 -1
  8. package/dist/errors.js +2 -2
  9. package/dist/errors.js.map +1 -1
  10. package/dist/interface/account.d.ts +6 -0
  11. package/dist/interface/download.d.ts +14 -0
  12. package/dist/internal/apiService/driveTypes.d.ts +197 -22
  13. package/dist/internal/download/controller.d.ts +3 -0
  14. package/dist/internal/download/controller.js +7 -0
  15. package/dist/internal/download/controller.js.map +1 -1
  16. package/dist/internal/download/cryptoService.js +9 -2
  17. package/dist/internal/download/cryptoService.js.map +1 -1
  18. package/dist/internal/download/fileDownloader.js +9 -3
  19. package/dist/internal/download/fileDownloader.js.map +1 -1
  20. package/dist/internal/download/fileDownloader.test.js +14 -11
  21. package/dist/internal/download/fileDownloader.test.js.map +1 -1
  22. package/dist/internal/download/interface.d.ts +14 -0
  23. package/dist/internal/download/interface.js +16 -0
  24. package/dist/internal/download/interface.js.map +1 -1
  25. package/dist/internal/nodes/apiService.d.ts +2 -1
  26. package/dist/internal/nodes/apiService.js +5 -2
  27. package/dist/internal/nodes/apiService.js.map +1 -1
  28. package/dist/internal/nodes/cryptoService.d.ts +1 -0
  29. package/dist/internal/nodes/cryptoService.js +28 -4
  30. package/dist/internal/nodes/cryptoService.js.map +1 -1
  31. package/dist/internal/nodes/cryptoService.test.js +70 -2
  32. package/dist/internal/nodes/cryptoService.test.js.map +1 -1
  33. package/dist/internal/nodes/nodesManagement.d.ts +2 -1
  34. package/dist/internal/nodes/nodesManagement.js +6 -1
  35. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  36. package/dist/internal/shares/apiService.js +2 -0
  37. package/dist/internal/shares/apiService.js.map +1 -1
  38. package/dist/internal/sharingPublic/nodes.d.ts +1 -1
  39. package/dist/internal/sharingPublic/nodes.js +2 -2
  40. package/dist/internal/sharingPublic/nodes.js.map +1 -1
  41. package/dist/internal/upload/apiService.d.ts +1 -0
  42. package/dist/internal/upload/apiService.js +12 -0
  43. package/dist/internal/upload/apiService.js.map +1 -1
  44. package/dist/internal/upload/manager.js +19 -1
  45. package/dist/internal/upload/manager.js.map +1 -1
  46. package/dist/internal/upload/manager.test.js +23 -0
  47. package/dist/internal/upload/manager.test.js.map +1 -1
  48. package/dist/internal/upload/streamUploader.js +1 -1
  49. package/dist/internal/upload/streamUploader.js.map +1 -1
  50. package/dist/protonDriveClient.d.ts +1 -1
  51. package/dist/protonDriveClient.js +3 -3
  52. package/dist/protonDriveClient.js.map +1 -1
  53. package/dist/protonDrivePhotosClient.js +1 -1
  54. package/dist/protonDrivePhotosClient.js.map +1 -1
  55. package/dist/protonDrivePublicLinkClient.d.ts +3 -1
  56. package/dist/protonDrivePublicLinkClient.js +4 -2
  57. package/dist/protonDrivePublicLinkClient.js.map +1 -1
  58. package/package.json +1 -1
  59. package/src/crypto/driveCrypto.ts +1 -0
  60. package/src/crypto/interface.ts +1 -0
  61. package/src/crypto/openPGPCrypto.ts +3 -0
  62. package/src/errors.ts +2 -2
  63. package/src/interface/account.ts +6 -0
  64. package/src/interface/download.ts +16 -0
  65. package/src/internal/apiService/driveTypes.ts +197 -22
  66. package/src/internal/download/controller.ts +9 -0
  67. package/src/internal/download/cryptoService.ts +13 -3
  68. package/src/internal/download/fileDownloader.test.ts +17 -11
  69. package/src/internal/download/fileDownloader.ts +9 -5
  70. package/src/internal/download/interface.ts +15 -0
  71. package/src/internal/nodes/apiService.ts +20 -6
  72. package/src/internal/nodes/cryptoService.test.ts +113 -2
  73. package/src/internal/nodes/cryptoService.ts +53 -8
  74. package/src/internal/nodes/nodesManagement.ts +7 -1
  75. package/src/internal/shares/apiService.ts +3 -1
  76. package/src/internal/sharingPublic/nodes.ts +2 -2
  77. package/src/internal/upload/apiService.ts +25 -0
  78. package/src/internal/upload/manager.test.ts +37 -0
  79. package/src/internal/upload/manager.ts +17 -1
  80. package/src/internal/upload/streamUploader.ts +1 -1
  81. package/src/protonDriveClient.ts +3 -3
  82. package/src/protonDrivePhotosClient.ts +1 -1
  83. package/src/protonDrivePublicLinkClient.ts +4 -2
@@ -20,6 +20,9 @@ describe('nodesCryptoService', () => {
20
20
 
21
21
  let cryptoService: NodesCryptoService;
22
22
 
23
+ const publicAddressKey = { _idx: 21312 };
24
+ const ownPrivateAddressKey = { id: 'id', key: 'key' as unknown as PrivateKey };
25
+
23
26
  beforeEach(() => {
24
27
  jest.clearAllMocks();
25
28
 
@@ -71,7 +74,15 @@ describe('nodesCryptoService', () => {
71
74
  };
72
75
  account = {
73
76
  // @ts-expect-error No need to implement all methods for mocking
74
- getPublicKeys: jest.fn(async () => [{ _idx: 21312 }]),
77
+ getPublicKeys: jest.fn(async () => [publicAddressKey]),
78
+ getOwnAddresses: jest.fn(async () => [
79
+ {
80
+ email: 'email',
81
+ addressId: 'addressId',
82
+ primaryKeyIndex: 0,
83
+ keys: [ownPrivateAddressKey],
84
+ },
85
+ ]),
75
86
  };
76
87
  // @ts-expect-error No need to implement all methods for mocking
77
88
  sharesService = {
@@ -576,6 +587,7 @@ describe('nodesCryptoService', () => {
576
587
  armoredNodePassphraseSignature: 'armoredNodePassphraseSignature',
577
588
  file: {
578
589
  base64ContentKeyPacket: 'base64ContentKeyPacket',
590
+ armoredContentKeyPacketSignature: 'armoredContentKeyPacketSignature',
579
591
  },
580
592
  activeRevision: {
581
593
  uid: 'revisionUid',
@@ -764,7 +776,7 @@ describe('nodesCryptoService', () => {
764
776
  });
765
777
  });
766
778
 
767
- it('on content key packet', async () => {
779
+ it('on content key packet without fallback verification', async () => {
768
780
  driveCrypto.decryptAndVerifySessionKey = jest.fn(
769
781
  async () =>
770
782
  Promise.resolve({
@@ -789,6 +801,105 @@ describe('nodesCryptoService', () => {
789
801
  error: 'verification error',
790
802
  });
791
803
  });
804
+
805
+ it('on content key packet with successful fallback verification', async () => {
806
+ driveCrypto.decryptAndVerifySessionKey = jest
807
+ .fn()
808
+ .mockImplementationOnce(
809
+ async () =>
810
+ Promise.resolve({
811
+ sessionKey: 'contentKeyPacketSessionKey',
812
+ verified: VERIFICATION_STATUS.SIGNED_AND_INVALID,
813
+ verificationErrors: [new Error('verification error')],
814
+ }) as any,
815
+ )
816
+ .mockImplementationOnce(
817
+ async () =>
818
+ Promise.resolve({
819
+ sessionKey: 'contentKeyPacketSessionKey',
820
+ verified: VERIFICATION_STATUS.SIGNED_AND_VALID,
821
+ }) as any,
822
+ );
823
+
824
+ const result = await cryptoService.decryptNode(
825
+ {
826
+ ...encryptedNode,
827
+ creationTime: new Date('2022-01-01'),
828
+ },
829
+ parentKey,
830
+ );
831
+ verifyResult(result);
832
+ expect(driveCrypto.decryptAndVerifySessionKey).toHaveBeenCalledTimes(2);
833
+ expect(driveCrypto.decryptAndVerifySessionKey).toHaveBeenCalledWith(
834
+ 'base64ContentKeyPacket',
835
+ 'armoredContentKeyPacketSignature',
836
+ 'decryptedKey',
837
+ ['decryptedKey', publicAddressKey],
838
+ );
839
+ expect(driveCrypto.decryptAndVerifySessionKey).toHaveBeenCalledWith(
840
+ 'base64ContentKeyPacket',
841
+ 'armoredContentKeyPacketSignature',
842
+ 'decryptedKey',
843
+ [ownPrivateAddressKey.key],
844
+ );
845
+ expect(telemetry.recordMetric).not.toHaveBeenCalled();
846
+ });
847
+
848
+ it('on content key packet with failed fallback verification', async () => {
849
+ driveCrypto.decryptAndVerifySessionKey = jest
850
+ .fn()
851
+ .mockImplementationOnce(
852
+ async () =>
853
+ Promise.resolve({
854
+ sessionKey: 'contentKeyPacketSessionKey',
855
+ verified: VERIFICATION_STATUS.SIGNED_AND_INVALID,
856
+ verificationErrors: [new Error('verification error')],
857
+ }) as any,
858
+ )
859
+ .mockImplementationOnce(
860
+ async () =>
861
+ Promise.resolve({
862
+ sessionKey: 'contentKeyPacketSessionKey',
863
+ verified: VERIFICATION_STATUS.SIGNED_AND_INVALID,
864
+ verificationErrors: [new Error('fallback verification error')],
865
+ }) as any,
866
+ );
867
+
868
+ const result = await cryptoService.decryptNode(
869
+ {
870
+ ...encryptedNode,
871
+ creationTime: new Date('2022-01-01'),
872
+ },
873
+ parentKey,
874
+ );
875
+ verifyResult(result, {
876
+ keyAuthor: {
877
+ ok: false,
878
+ error: {
879
+ claimedAuthor: 'signatureEmail',
880
+ error: 'Signature verification for content key failed: verification error',
881
+ },
882
+ },
883
+ });
884
+ expect(driveCrypto.decryptAndVerifySessionKey).toHaveBeenCalledTimes(2);
885
+ expect(driveCrypto.decryptAndVerifySessionKey).toHaveBeenCalledWith(
886
+ 'base64ContentKeyPacket',
887
+ 'armoredContentKeyPacketSignature',
888
+ 'decryptedKey',
889
+ ['decryptedKey', publicAddressKey],
890
+ );
891
+ expect(driveCrypto.decryptAndVerifySessionKey).toHaveBeenCalledWith(
892
+ 'base64ContentKeyPacket',
893
+ 'armoredContentKeyPacketSignature',
894
+ 'decryptedKey',
895
+ [ownPrivateAddressKey.key],
896
+ );
897
+ verifyLogEventVerificationError({
898
+ field: 'nodeContentKey',
899
+ error: 'verification error',
900
+ fromBefore2024: true,
901
+ });
902
+ });
792
903
  });
793
904
 
794
905
  describe('should decrypt with decryption issues', () => {
@@ -25,6 +25,7 @@ import {
25
25
  EncryptedRevision,
26
26
  DecryptedUnparsedRevision,
27
27
  NodeSigningKeys,
28
+ EncryptedNodeFileCrypto,
28
29
  } from './interface';
29
30
 
30
31
  export interface NodesCryptoReporter {
@@ -200,14 +201,7 @@ export class NodesCryptoService {
200
201
  if ('file' in node.encryptedCrypto) {
201
202
  const [activeRevisionPromise, contentKeyPacketSessionKeyPromise] = [
202
203
  this.decryptRevision(node.uid, node.encryptedCrypto.activeRevision, key),
203
- this.driveCrypto.decryptAndVerifySessionKey(
204
- node.encryptedCrypto.file.base64ContentKeyPacket,
205
- node.encryptedCrypto.file.armoredContentKeyPacketSignature,
206
- key,
207
- // Content key packet is signed with the node key, but
208
- // in the past some clients signed with the address key.
209
- [key, ...keyVerificationKeys],
210
- ),
204
+ this.decryptContentKeyPacket(node, node.encryptedCrypto, key, keyVerificationKeys),
211
205
  ];
212
206
 
213
207
  try {
@@ -502,6 +496,57 @@ export class NodesCryptoService {
502
496
  };
503
497
  }
504
498
 
499
+ private async decryptContentKeyPacket(
500
+ node: EncryptedNode,
501
+ encryptedCrypto: EncryptedNodeFileCrypto,
502
+ key: PrivateKey,
503
+ keyVerificationKeys: PublicKey[],
504
+ ): Promise<{
505
+ sessionKey: SessionKey;
506
+ verified?: VERIFICATION_STATUS;
507
+ verificationErrors?: Error[];
508
+ }> {
509
+ const result = await this.driveCrypto.decryptAndVerifySessionKey(
510
+ encryptedCrypto.file.base64ContentKeyPacket,
511
+ encryptedCrypto.file.armoredContentKeyPacketSignature,
512
+ key,
513
+ // Content key packet is signed with the node key, but
514
+ // in the past some clients signed with the address key.
515
+ [key, ...keyVerificationKeys],
516
+ );
517
+
518
+ // Return right away if the verification is signed or not signed.
519
+ // If the verification is failing and the file is before 2023, try
520
+ // to decrypt with all owners keys. Because of the old nodes signed
521
+ // with address key instead of node key, when the node was renamed
522
+ // or moved, it could change the address but without updating the
523
+ // content key packet, which is now failing.
524
+ if (result.verified !== VERIFICATION_STATUS.SIGNED_AND_INVALID || node.creationTime > new Date(2023, 0, 1)) {
525
+ return result;
526
+ }
527
+
528
+ const allAddresses = await this.account.getOwnAddresses();
529
+ const allKeys = allAddresses.flatMap((address) => address.keys.map(({ key }) => key));
530
+
531
+ const resultWithAllKeys = await this.driveCrypto.decryptAndVerifySessionKey(
532
+ encryptedCrypto.file.base64ContentKeyPacket,
533
+ encryptedCrypto.file.armoredContentKeyPacketSignature,
534
+ key,
535
+ // Content key packet is signed with the node key, but
536
+ // in the past some clients signed with the address key.
537
+ allKeys,
538
+ );
539
+
540
+ // Return original result with original error if the fallback verification also fails.
541
+ if (resultWithAllKeys.verified === VERIFICATION_STATUS.SIGNED_AND_VALID) {
542
+ this.logger.warn(
543
+ 'Content key packet signature verification failed, but fallback to all addresses succeeded',
544
+ );
545
+ return resultWithAllKeys;
546
+ }
547
+ return result;
548
+ }
549
+
505
550
  private async decryptExtendedAttributes(
506
551
  node: { uid: string; creationTime: Date },
507
552
  encryptedExtendedAttributes: string | undefined,
@@ -127,6 +127,12 @@ export abstract class NodesManagementBase<
127
127
  }
128
128
  }
129
129
 
130
+ async emptyTrash(): Promise<void> {
131
+ const node = await this.nodesAccess.getVolumeRootFolder();
132
+ const { volumeId } = splitNodeUid(node.uid);
133
+ await this.apiService.emptyTrash(volumeId);
134
+ }
135
+
130
136
  async moveNode(nodeUid: string, newParentUid: string): Promise<TDecryptedNode> {
131
137
  const node = await this.nodesAccess.getNode(nodeUid);
132
138
 
@@ -293,7 +299,7 @@ export abstract class NodesManagementBase<
293
299
  }
294
300
  }
295
301
 
296
- async *deleteNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
302
+ async *deleteTrashedNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
297
303
  for await (const result of this.apiService.deleteTrashedNodes(nodeUids, signal)) {
298
304
  if (result.ok) {
299
305
  await this.nodesAccess.notifyNodeDeleted(result.uid);
@@ -173,7 +173,7 @@ function convertSharePayload(response: GetShareResponse): EncryptedShare {
173
173
  };
174
174
  }
175
175
 
176
- function convertShareTypeNumberToEnum(type: 1 | 2 | 3 | 4): ShareType {
176
+ function convertShareTypeNumberToEnum(type: 1 | 2 | 3 | 4 | 5): ShareType {
177
177
  switch (type) {
178
178
  case 1:
179
179
  return ShareType.Main;
@@ -183,5 +183,7 @@ function convertShareTypeNumberToEnum(type: 1 | 2 | 3 | 4): ShareType {
183
183
  return ShareType.Device;
184
184
  case 4:
185
185
  return ShareType.Photo;
186
+ case 5:
187
+ throw new Error('Organization shares are not supported yet');
186
188
  }
187
189
  }
@@ -87,10 +87,10 @@ export class SharingPublicNodesManagement extends NodesManagement {
87
87
  super(apiService, cryptoCache, cryptoService, nodesAccess);
88
88
  }
89
89
 
90
- async *deleteNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
90
+ async *deleteMyNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
91
91
  // Public link does not support trashing and deleting trashed nodes.
92
92
  // Instead, if user is owner, API allows directly deleting existing nodes.
93
- for await (const result of this.apiService.deleteExistingNodes(nodeUids, signal)) {
93
+ for await (const result of this.apiService.deleteMyNodes(nodeUids, signal)) {
94
94
  if (result.ok) {
95
95
  await this.nodesAccess.notifyNodeDeleted(result.uid);
96
96
  }
@@ -45,6 +45,13 @@ type PostDeleteNodesRequest = Extract<
45
45
  type PostDeleteNodesResponse =
46
46
  drivePaths['/drive/v2/volumes/{volumeID}/delete_multiple']['post']['responses']['200']['content']['application/json'];
47
47
 
48
+ type PostLoadLinksMetadataRequest = Extract<
49
+ drivePaths['/drive/v2/volumes/{volumeID}/links']['post']['requestBody'],
50
+ { content: object }
51
+ >['content']['application/json'];
52
+ type PostLoadLinksMetadataResponse =
53
+ drivePaths['/drive/v2/volumes/{volumeID}/links']['post']['responses']['200']['content']['application/json'];
54
+
48
55
  export class UploadAPIService {
49
56
  constructor(
50
57
  protected apiService: DriveAPIService,
@@ -262,4 +269,22 @@ export class UploadAPIService {
262
269
 
263
270
  await this.apiService.postBlockStream(url, token, formData, onProgress, signal);
264
271
  }
272
+
273
+ async isRevisionUploaded(nodeRevisionUid: string): Promise<boolean> {
274
+ const { volumeId, nodeId, revisionId } = splitNodeRevisionUid(nodeRevisionUid);
275
+ const result = await this.apiService.post<PostLoadLinksMetadataRequest, PostLoadLinksMetadataResponse>(
276
+ `drive/v2/volumes/${volumeId}/links`,
277
+ {
278
+ LinkIDs: [nodeId],
279
+ },
280
+ );
281
+ if (result.Links.length === 0) {
282
+ return false;
283
+ }
284
+ const link = result.Links[0];
285
+ return (
286
+ link.Link.State === 1 && // ACTIVE state
287
+ link.File?.ActiveRevision?.RevisionID === revisionId
288
+ );
289
+ }
265
290
  }
@@ -327,5 +327,42 @@ describe('UploadManager', () => {
327
327
  expect(nodesService.notifyChildCreated).toHaveBeenCalledWith('parentUid');
328
328
  expect(nodesService.notifyNodeChanged).not.toHaveBeenCalled();
329
329
  });
330
+
331
+ it('should ignore error if revision was committed successfully', async () => {
332
+ apiService.commitDraftRevision = jest
333
+ .fn()
334
+ .mockRejectedValue(new Error('Revision to commit must be a draft'));
335
+ apiService.isRevisionUploaded = jest.fn().mockResolvedValue(true);
336
+
337
+ await manager.commitDraft(nodeRevisionDraft as any, manifest, extendedAttributes);
338
+
339
+ expect(apiService.commitDraftRevision).toHaveBeenCalledWith(
340
+ nodeRevisionDraft.nodeRevisionUid,
341
+ expect.anything(),
342
+ );
343
+ expect(nodesService.notifyNodeChanged).toHaveBeenCalled();
344
+ });
345
+
346
+ it('should throw error if revision was not committed successfully', async () => {
347
+ apiService.commitDraftRevision = jest
348
+ .fn()
349
+ .mockRejectedValue(new Error('Revision to commit must be a draft'));
350
+ apiService.isRevisionUploaded = jest.fn().mockResolvedValue(false);
351
+
352
+ await expect(manager.commitDraft(nodeRevisionDraft as any, manifest, extendedAttributes)).rejects.toThrow(
353
+ 'Revision to commit must be a draft',
354
+ );
355
+ expect(nodesService.notifyNodeChanged).not.toHaveBeenCalled();
356
+ });
357
+
358
+ it('should throw original error if revision cannot be verified', async () => {
359
+ apiService.commitDraftRevision = jest.fn().mockRejectedValue(new Error('Failed to commit revision'));
360
+ apiService.isRevisionUploaded = jest.fn().mockRejectedValue(new Error('Failed to verify revision'));
361
+
362
+ await expect(manager.commitDraft(nodeRevisionDraft as any, manifest, extendedAttributes)).rejects.toThrow(
363
+ 'Failed to commit revision',
364
+ );
365
+ expect(nodesService.notifyNodeChanged).not.toHaveBeenCalled();
366
+ });
330
367
  });
331
368
  });
@@ -246,7 +246,23 @@ export class UploadManager {
246
246
  manifest,
247
247
  generatedExtendedAttributes,
248
248
  );
249
- await this.apiService.commitDraftRevision(nodeRevisionDraft.nodeRevisionUid, nodeCommitCrypto);
249
+ try {
250
+ await this.apiService.commitDraftRevision(nodeRevisionDraft.nodeRevisionUid, nodeCommitCrypto);
251
+ } catch (error: unknown) {
252
+ // Commit might be sent but due to network error no response is
253
+ // received. In this case, API service automatically retries the
254
+ // request. If the first attempt passed, it will fail on the second
255
+ // attempt. We need to check if the revision was actually committed.
256
+ try {
257
+ const isRevisionUploaded = await this.apiService.isRevisionUploaded(nodeRevisionDraft.nodeRevisionUid);
258
+ if (!isRevisionUploaded) {
259
+ throw error;
260
+ }
261
+ } catch {
262
+ throw error; // Throw original error, not the checking one.
263
+ }
264
+ this.logger.warn(`Node commit failed but node was committed successfully ${nodeRevisionDraft.nodeUid}`);
265
+ }
250
266
  await this.notifyNodeUploaded(nodeRevisionDraft);
251
267
  }
252
268
 
@@ -181,7 +181,7 @@ export class StreamUploader {
181
181
  await this.controller.waitWhilePaused();
182
182
  await this.waitForUploadCapacityAndBufferedBlocks();
183
183
 
184
- if (this.isEncryptionFullyFinished) {
184
+ if (this.isEncryptionFullyFinished || this.isUploadAborted) {
185
185
  break;
186
186
  }
187
187
 
@@ -495,7 +495,7 @@ export class ProtonDriveClient {
495
495
  }
496
496
 
497
497
  /**
498
- * Delete the nodes permanently.
498
+ * Delete the trashed nodes permanently. Only the owner can do that.
499
499
  *
500
500
  * The operation is performed in batches and the results are yielded
501
501
  * as they are available. Order of the results is not guaranteed.
@@ -509,12 +509,12 @@ export class ProtonDriveClient {
509
509
  */
510
510
  async *deleteNodes(nodeUids: NodeOrUid[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
511
511
  this.logger.info(`Deleting ${nodeUids.length} nodes`);
512
- yield* this.nodes.management.deleteNodes(getUids(nodeUids), signal);
512
+ yield* this.nodes.management.deleteTrashedNodes(getUids(nodeUids), signal);
513
513
  }
514
514
 
515
515
  async emptyTrash(): Promise<void> {
516
516
  this.logger.info('Emptying trash');
517
- throw new Error('Method not implemented');
517
+ return this.nodes.management.emptyTrash();
518
518
  }
519
519
 
520
520
  /**
@@ -295,7 +295,7 @@ export class ProtonDrivePhotosClient {
295
295
  */
296
296
  async *deleteNodes(nodeUids: NodeOrUid[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
297
297
  this.logger.info(`Deleting ${nodeUids.length} nodes`);
298
- yield* this.nodes.management.deleteNodes(getUids(nodeUids), signal);
298
+ yield * this.nodes.management.deleteTrashedNodes(getUids(nodeUids), signal);
299
299
  }
300
300
 
301
301
  /**
@@ -233,13 +233,15 @@ export class ProtonDrivePublicLinkClient {
233
233
  }
234
234
 
235
235
  /**
236
- * Delete the nodes permanently.
236
+ * Delete own nodes permanently. It skips the trash and allows to delete
237
+ * only nodes that are owned by the user. For anonymous files, this method
238
+ * allows to delete them only in the same session.
237
239
  *
238
240
  * See `ProtonDriveClient.deleteNodes` for more information.
239
241
  */
240
242
  async *deleteNodes(nodeUids: NodeOrUid[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
241
243
  this.logger.info(`Deleting ${nodeUids.length} nodes`);
242
- yield* this.sharingPublic.nodes.management.deleteNodes(getUids(nodeUids), signal);
244
+ yield* this.sharingPublic.nodes.management.deleteMyNodes(getUids(nodeUids), signal);
243
245
  }
244
246
 
245
247
  /**