@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
@@ -491,7 +491,8 @@ export interface paths {
491
491
  get?: never;
492
492
  put?: never;
493
493
  /**
494
- * Delete children
494
+ * Delete drafts from folder
495
+ * @deprecated
495
496
  * @description Permanently delete children from folder, skipping trash. Can only be done for draft links.
496
497
  */
497
498
  post: operations["post_drive-shares-{shareID}-folders-{linkID}-delete_multiple"];
@@ -531,8 +532,8 @@ export interface paths {
531
532
  get?: never;
532
533
  put?: never;
533
534
  /**
534
- * Trash children
535
- * @description Send children to trash
535
+ * Trash children from folder
536
+ * @deprecated
536
537
  */
537
538
  post: operations["post_drive-shares-{shareID}-folders-{linkID}-trash_multiple"];
538
539
  delete?: never;
@@ -672,8 +673,8 @@ export interface paths {
672
673
  get?: never;
673
674
  put?: never;
674
675
  /**
675
- * Delete multiple (v2)
676
- * @description Permanently delete links, skipping trash. Can only be done for draft links.
676
+ * Delete drafts
677
+ * @description Permanently delete files, skipping trash. Can only be done for draft links.
677
678
  */
678
679
  post: operations["post_drive-v2-volumes-{volumeID}-delete_multiple"];
679
680
  delete?: never;
@@ -815,6 +816,32 @@ export interface paths {
815
816
  patch?: never;
816
817
  trace?: never;
817
818
  };
819
+ "/drive/v2/volumes/{volumeID}/remove-mine": {
820
+ parameters: {
821
+ query?: never;
822
+ header?: never;
823
+ path?: never;
824
+ cookie?: never;
825
+ };
826
+ get?: never;
827
+ put?: never;
828
+ /**
829
+ * Remove my nodes skipping trash
830
+ * @description This is called by Web SDK on public sharing to remove active nodes created by the same user
831
+ * as a way to delete wrongly uploaded files without going to trash. It's supported on the following conditions:
832
+ * - anonymous users must have created the node in their own session
833
+ * - for authenticated users the signature email must match
834
+ * - file/folder must have been created within the last 1 hour
835
+ * - folders must be empty
836
+ * - files must have all revisions created by this user
837
+ */
838
+ post: operations["post_drive-v2-volumes-{volumeID}-remove-mine"];
839
+ delete?: never;
840
+ options?: never;
841
+ head?: never;
842
+ patch?: never;
843
+ trace?: never;
844
+ };
818
845
  "/drive/v2/volumes/{volumeID}/links/{linkID}/rename": {
819
846
  parameters: {
820
847
  query?: never;
@@ -829,6 +856,11 @@ export interface paths {
829
856
  *
830
857
  * Clients renaming a file or folder MUST reuse the existing session key
831
858
  * for the name as it is also used by shares pointing to the link.
859
+ *
860
+ * Users with access only through a public sharing URL (no editor membership) are limited to renaming
861
+ * their own files and folders:
862
+ * - Unauthenticated users must have created them in their session
863
+ * - Authenticated users' email must match the signature email on the node for folders or active revision for files
832
864
  */
833
865
  put: operations["put_drive-v2-volumes-{volumeID}-links-{linkID}-rename"];
834
866
  post?: never;
@@ -852,6 +884,11 @@ export interface paths {
852
884
  *
853
885
  * Clients renaming a file or folder MUST reuse the existing session key
854
886
  * for the name as it is also used by shares pointing to the link.
887
+ *
888
+ * Users with access only through a public sharing URL (no editor membership) are limited to renaming
889
+ * their own files and folders:
890
+ * - Unauthenticated users must have created them in their session
891
+ * - Authenticated users' email must match the signature email on the node for folders or active revision for files
855
892
  */
856
893
  put: operations["put_drive-shares-{shareID}-links-{linkID}-rename"];
857
894
  post?: never;
@@ -2227,7 +2264,7 @@ export interface paths {
2227
2264
  get?: never;
2228
2265
  put?: never;
2229
2266
  /**
2230
- * Delete multiple (v2)
2267
+ * Delete drafts
2231
2268
  * @description See /drive/v2/volumes/{volumeID}/delete_multiple for full documentation
2232
2269
  */
2233
2270
  post: operations["post_drive-unauth-v2-volumes-{volumeID}-delete_multiple"];
@@ -2297,6 +2334,26 @@ export interface paths {
2297
2334
  patch?: never;
2298
2335
  trace?: never;
2299
2336
  };
2337
+ "/drive/unauth/v2/volumes/{volumeID}/remove-mine": {
2338
+ parameters: {
2339
+ query?: never;
2340
+ header?: never;
2341
+ path?: never;
2342
+ cookie?: never;
2343
+ };
2344
+ get?: never;
2345
+ put?: never;
2346
+ /**
2347
+ * Remove my nodes skipping trash
2348
+ * @description See /drive/v2/volumes/{volumeID}/remove-mine for full documentation
2349
+ */
2350
+ post: operations["post_drive-unauth-v2-volumes-{volumeID}-remove-mine"];
2351
+ delete?: never;
2352
+ options?: never;
2353
+ head?: never;
2354
+ patch?: never;
2355
+ trace?: never;
2356
+ };
2300
2357
  "/drive/unauth/v2/volumes/{volumeID}/links/{linkID}/rename": {
2301
2358
  parameters: {
2302
2359
  query?: never;
@@ -2978,6 +3035,30 @@ export interface paths {
2978
3035
  patch?: never;
2979
3036
  trace?: never;
2980
3037
  };
3038
+ "/drive/organization/volumes": {
3039
+ parameters: {
3040
+ query?: never;
3041
+ header?: never;
3042
+ path?: never;
3043
+ cookie?: never;
3044
+ };
3045
+ get?: never;
3046
+ put?: never;
3047
+ /**
3048
+ * Create Organization volume
3049
+ * @description Only allowed to Org administrators
3050
+ *
3051
+ * This new volume would have:
3052
+ * + OwnerOrgID filled with the orgID of the request
3053
+ * + specific membership for the owner (OrgAdmin to true)
3054
+ */
3055
+ post: operations["post_drive-organization-volumes"];
3056
+ delete?: never;
3057
+ options?: never;
3058
+ head?: never;
3059
+ patch?: never;
3060
+ trace?: never;
3061
+ };
2981
3062
  "/drive/volumes": {
2982
3063
  parameters: {
2983
3064
  query?: never;
@@ -4852,6 +4933,31 @@ export interface components {
4852
4933
  /** @description Order and visibility of Photo Tags, tags not in the list should not be shown; Use defaults when NULL; Show no tags if empty array. */
4853
4934
  PhotoTags?: components["schemas"]["TagType"][] | null;
4854
4935
  };
4936
+ CreateOrgVolumeRequestDto: {
4937
+ AddressID: components["schemas"]["AddressID"];
4938
+ /** @description XX's encrypted AddressKeyID. Must be the primary key from the AddressID */
4939
+ AddressKeyID: string;
4940
+ ShareKey: components["schemas"]["PGPPrivateKey"];
4941
+ SharePassphrase: components["schemas"]["PGPMessage"];
4942
+ SharePassphraseSignature: components["schemas"]["PGPSignature"];
4943
+ FolderName: components["schemas"]["PGPMessage"];
4944
+ FolderKey: components["schemas"]["PGPPrivateKey"];
4945
+ FolderPassphrase: components["schemas"]["PGPMessage"];
4946
+ FolderPassphraseSignature: components["schemas"]["PGPSignature"];
4947
+ FolderHashKey: components["schemas"]["PGPMessage"];
4948
+ OrganizationID: components["schemas"]["Id"];
4949
+ /** @description Name of the org. volume. It's plain text so that name can be displayed in UI menu */
4950
+ VolumeName: string;
4951
+ };
4952
+ GetVolumeResponseDto: {
4953
+ Volume: components["schemas"]["VolumeResponseDto"];
4954
+ /**
4955
+ * ProtonResponseCode
4956
+ * @example 1000
4957
+ * @enum {integer}
4958
+ */
4959
+ Code: 1000;
4960
+ };
4855
4961
  CreateVolumeRequestDto: {
4856
4962
  AddressID: components["schemas"]["AddressID"];
4857
4963
  ShareKey: components["schemas"]["PGPPrivateKey"];
@@ -4875,15 +4981,6 @@ export interface components {
4875
4981
  */
4876
4982
  ShareName: string | null;
4877
4983
  };
4878
- GetVolumeResponseDto: {
4879
- Volume: components["schemas"]["VolumeResponseDto"];
4880
- /**
4881
- * ProtonResponseCode
4882
- * @example 1000
4883
- * @enum {integer}
4884
- */
4885
- Code: 1000;
4886
- };
4887
4984
  ListVolumesResponseDto: {
4888
4985
  Volumes: components["schemas"]["VolumeResponseDto"][];
4889
4986
  /**
@@ -6213,20 +6310,20 @@ export interface components {
6213
6310
  Album: null | null;
6214
6311
  };
6215
6312
  /**
6216
- * @description <p>1=Main, 2=Standard, 3=Device, 4=Photo</p><details><summary>See values descriptions</summary><details><summary>See values descriptions</summary><table><tr><th>Value</th><th>Name</th><th>Description</th></tr><tr><td>1</td><td>Main</td><td>* Root share for my files</td></tr><tr><td>2</td><td>Standard</td><td>* Collaborative share anywhere in the link tree (but not at the root folder as it cannot be shared)</td></tr><tr><td>3</td><td>Device</td><td>* Root share of devices</td></tr><tr><td>4</td><td>Photo</td><td>* Root share for photos</td></tr></table></details></details>
6313
+ * @description <p>1=Main, 2=Standard, 3=Device, 4=Photo</p><details><summary>See values descriptions</summary><details><summary>See values descriptions</summary><table><tr><th>Value</th><th>Name</th><th>Description</th></tr><tr><td>1</td><td>Main</td><td>* Root share for my files</td></tr><tr><td>2</td><td>Standard</td><td>* Collaborative share anywhere in the link tree (but not at the root folder as it cannot be shared)</td></tr><tr><td>3</td><td>Device</td><td>* Root share of devices</td></tr><tr><td>4</td><td>Photo</td><td>* Root share for photos</td></tr><tr><td>5</td><td>Organization</td><td>* Root share for organization</td></tr></table></details></details>
6217
6314
  * @enum {integer}
6218
6315
  */
6219
- ShareType: 1 | 2 | 3 | 4;
6316
+ ShareType: 1 | 2 | 3 | 4 | 5;
6220
6317
  /**
6221
6318
  * @description <p>1=Active, 3=Restored</p><details><summary>See values descriptions</summary><details><summary>See values descriptions</summary><table><tr><th>Value</th><th>Description</th></tr><tr><td>1</td><td>Active</td></tr><tr><td>2</td><td>Deleted</td></tr><tr><td>3</td><td>Restored</td></tr><tr><td>6</td><td>Locked</td></tr></table></details></details>
6222
6319
  * @enum {integer}
6223
6320
  */
6224
6321
  ShareState: 1 | 2 | 3 | 6;
6225
6322
  /**
6226
- * @description <p>1=Regular, 2=Photo</p><details><summary>See values descriptions</summary><details><summary>See values descriptions</summary><table><tr><th>Value</th><th>Description</th></tr><tr><td>1</td><td>Regular</td></tr><tr><td>2</td><td>Photo</td></tr></table></details></details>
6323
+ * @description <p>1=Regular, 2=Photo</p><details><summary>See values descriptions</summary><details><summary>See values descriptions</summary><table><tr><th>Value</th><th>Description</th></tr><tr><td>1</td><td>Regular</td></tr><tr><td>2</td><td>Photo</td></tr><tr><td>3</td><td>Organization</td></tr></table></details></details>
6227
6324
  * @enum {integer}
6228
6325
  */
6229
- VolumeType: 1 | 2;
6326
+ VolumeType: 1 | 2 | 3;
6230
6327
  /**
6231
6328
  * @description <p>1=folder, 2=file</p><details><summary>See values descriptions</summary><details><summary>See values descriptions</summary><table><tr><th>Value</th><th>Description</th></tr><tr><td>1</td><td>Folder</td></tr><tr><td>2</td><td>File</td></tr><tr><td>3</td><td>Album</td></tr></table></details></details>
6232
6329
  * @enum {integer}
@@ -6776,10 +6873,10 @@ export interface components {
6776
6873
  LinkID: components["schemas"]["Id2"];
6777
6874
  };
6778
6875
  /**
6779
- * @description <details><summary>See values descriptions</summary><details><summary>See values descriptions</summary><table><tr><th>Value</th><th>Description</th></tr><tr><td>1</td><td>Regular</td></tr><tr><td>2</td><td>Photo</td></tr></table></details></details>
6876
+ * @description <details><summary>See values descriptions</summary><details><summary>See values descriptions</summary><table><tr><th>Value</th><th>Description</th></tr><tr><td>1</td><td>Regular</td></tr><tr><td>2</td><td>Photo</td></tr><tr><td>3</td><td>Organization</td></tr></table></details></details>
6780
6877
  * @enum {integer}
6781
6878
  */
6782
- VolumeType2: 1 | 2;
6879
+ VolumeType2: 1 | 2 | 3;
6783
6880
  /**
6784
6881
  * @description <p>Can be null if the Link was deleted</p><details><summary>See values descriptions</summary><details><summary>See values descriptions</summary><table><tr><th>Value</th><th>Description</th></tr><tr><td>0</td><td>Draft</td></tr><tr><td>1</td><td>Active</td></tr><tr><td>2</td><td>Trashed</td></tr></table></details></details>
6785
6882
  * @enum {integer}
@@ -9119,6 +9216,36 @@ export interface operations {
9119
9216
  };
9120
9217
  };
9121
9218
  };
9219
+ "post_drive-v2-volumes-{volumeID}-remove-mine": {
9220
+ parameters: {
9221
+ query?: never;
9222
+ header?: never;
9223
+ path: {
9224
+ volumeID: string;
9225
+ };
9226
+ cookie?: never;
9227
+ };
9228
+ requestBody?: {
9229
+ content: {
9230
+ "application/json": components["schemas"]["LinkIDsRequestDto"];
9231
+ };
9232
+ };
9233
+ responses: {
9234
+ /** @description Ok */
9235
+ 200: {
9236
+ headers: {
9237
+ [name: string]: unknown;
9238
+ };
9239
+ content: {
9240
+ "application/json": {
9241
+ /** @enum {integer} */
9242
+ Code?: 1001;
9243
+ Responses?: components["schemas"]["MultiDeleteTransformer"][];
9244
+ };
9245
+ };
9246
+ };
9247
+ };
9248
+ };
9122
9249
  "put_drive-v2-volumes-{volumeID}-links-{linkID}-rename": {
9123
9250
  parameters: {
9124
9251
  query?: never;
@@ -12150,6 +12277,29 @@ export interface operations {
12150
12277
  };
12151
12278
  };
12152
12279
  };
12280
+ "post_drive-unauth-v2-volumes-{volumeID}-remove-mine": {
12281
+ parameters: {
12282
+ query?: never;
12283
+ header?: never;
12284
+ path: {
12285
+ volumeID: string;
12286
+ };
12287
+ cookie?: never;
12288
+ };
12289
+ requestBody?: {
12290
+ content: {
12291
+ "application/json": components["schemas"]["LinkIDsRequestDto"];
12292
+ };
12293
+ };
12294
+ responses: {
12295
+ default: {
12296
+ headers: {
12297
+ [name: string]: unknown;
12298
+ };
12299
+ content?: never;
12300
+ };
12301
+ };
12302
+ };
12153
12303
  "put_drive-unauth-v2-volumes-{volumeID}-links-{linkID}-rename": {
12154
12304
  parameters: {
12155
12305
  query?: never;
@@ -12404,7 +12554,7 @@ export interface operations {
12404
12554
  /** @description Show disabled shares as well, i.e. Shares where the ShareMemberShip for the user is non-active (locked), otherwise only return with active Membership */
12405
12555
  ShowAll?: 0 | 1;
12406
12556
  /** @description Filter on Share Type */
12407
- ShareType?: 1 | 2 | 3 | 4;
12557
+ ShareType?: 1 | 2 | 3 | 4 | 5;
12408
12558
  };
12409
12559
  header?: never;
12410
12560
  path?: never;
@@ -13477,6 +13627,31 @@ export interface operations {
13477
13627
  };
13478
13628
  };
13479
13629
  };
13630
+ "post_drive-organization-volumes": {
13631
+ parameters: {
13632
+ query?: never;
13633
+ header?: never;
13634
+ path?: never;
13635
+ cookie?: never;
13636
+ };
13637
+ requestBody?: {
13638
+ content: {
13639
+ "application/json": components["schemas"]["CreateOrgVolumeRequestDto"];
13640
+ };
13641
+ };
13642
+ responses: {
13643
+ /** @description Success */
13644
+ 200: {
13645
+ headers: {
13646
+ "x-pm-code": 1000;
13647
+ [name: string]: unknown;
13648
+ };
13649
+ content: {
13650
+ "application/json": components["schemas"]["GetVolumeResponseDto"];
13651
+ };
13652
+ };
13653
+ };
13654
+ };
13480
13655
  "get_drive-volumes": {
13481
13656
  parameters: {
13482
13657
  query?: {
@@ -4,6 +4,7 @@ import { waitForCondition } from '../wait';
4
4
  export class DownloadController {
5
5
  private paused = false;
6
6
  public promise?: Promise<void>;
7
+ private _isDownloadCompleteWithSignatureIssues = false;
7
8
 
8
9
  constructor(private signal?: AbortSignal) {
9
10
  this.signal = signal;
@@ -31,4 +32,12 @@ export class DownloadController {
31
32
  async completion(): Promise<void> {
32
33
  await this.promise;
33
34
  }
35
+
36
+ isDownloadCompleteWithSignatureIssues(): boolean {
37
+ return this._isDownloadCompleteWithSignatureIssues;
38
+ }
39
+
40
+ setIsDownloadCompleteWithSignatureIssues(value: boolean): void {
41
+ this._isDownloadCompleteWithSignatureIssues = value;
42
+ }
34
43
  }
@@ -12,7 +12,7 @@ import { ProtonDriveAccount, Revision } from '../../interface';
12
12
  import { DecryptionError, IntegrityError } from '../../errors';
13
13
  import { getErrorMessage } from '../errors';
14
14
  import { mergeUint8Arrays } from '../utils';
15
- import { RevisionKeys } from './interface';
15
+ import { RevisionKeys, SignatureVerificationError } from './interface';
16
16
 
17
17
  export class DownloadCryptoService {
18
18
  constructor(
@@ -90,13 +90,23 @@ export class DownloadCryptoService {
90
90
  allBlockHashes: Uint8Array[],
91
91
  armoredManifestSignature?: string,
92
92
  ): Promise<void> {
93
- const verificationKeys = await this.getRevisionVerificationKeys(revision, nodeKey);
94
93
  const hash = mergeUint8Arrays(allBlockHashes);
95
94
 
96
95
  if (!armoredManifestSignature) {
97
96
  throw new IntegrityError(c('Error').t`Missing integrity signature`);
98
97
  }
99
98
 
99
+ let verificationKeys;
100
+ try {
101
+ verificationKeys = await this.getRevisionVerificationKeys(revision, nodeKey);
102
+ } catch (error: unknown) {
103
+ throw new SignatureVerificationError(
104
+ c('Error').t`Failed to get verification keys`,
105
+ { revisionUid: revision.uid, contentAuthor: revision.contentAuthor },
106
+ { cause: error },
107
+ );
108
+ }
109
+
100
110
  const { verified, verificationErrors } = await this.driveCrypto.verifyManifest(
101
111
  hash,
102
112
  armoredManifestSignature,
@@ -104,7 +114,7 @@ export class DownloadCryptoService {
104
114
  );
105
115
 
106
116
  if (verified !== VERIFICATION_STATUS.SIGNED_AND_VALID) {
107
- throw new IntegrityError(c('Error').t`Data integrity check failed`, {
117
+ throw new SignatureVerificationError(c('Error').t`Data integrity check failed`, {
108
118
  verificationErrors,
109
119
  });
110
120
  }
@@ -4,6 +4,8 @@ import { FileDownloader } from './fileDownloader';
4
4
  import { DownloadTelemetry } from './telemetry';
5
5
  import { DownloadAPIService } from './apiService';
6
6
  import { DownloadCryptoService } from './cryptoService';
7
+ import { SignatureVerificationError } from './interface';
8
+ import { IntegrityError } from '../..';
7
9
 
8
10
  function mockBlockDownload(_: string, token: string, onProgress: (downloadedBytes: number) => void) {
9
11
  const index = parseInt(token.slice(5, 6));
@@ -94,8 +96,6 @@ describe('FileDownloader', () => {
94
96
 
95
97
  expect(apiService.iterateRevisionBlocks).toHaveBeenCalledWith('revisionUid', undefined);
96
98
  expect(cryptoService.verifyManifest).toHaveBeenCalledTimes(1);
97
- expect(writer.close).toHaveBeenCalledTimes(1);
98
- expect(writer.abort).not.toHaveBeenCalled();
99
99
  expect(telemetry.downloadFinished).toHaveBeenCalledTimes(1);
100
100
  expect(telemetry.downloadFinished).toHaveBeenCalledWith('revisionUid', fileProgress);
101
101
  expect(telemetry.downloadFailed).not.toHaveBeenCalled();
@@ -108,8 +108,6 @@ describe('FileDownloader', () => {
108
108
  await expect(controller.completion()).rejects.toThrow(error);
109
109
 
110
110
  expect(apiService.iterateRevisionBlocks).toHaveBeenCalledWith('revisionUid', undefined);
111
- expect(writer.close).not.toHaveBeenCalled();
112
- expect(writer.abort).toHaveBeenCalledTimes(1);
113
111
  expect(telemetry.downloadFinished).not.toHaveBeenCalled();
114
112
  expect(telemetry.downloadFailed).toHaveBeenCalledTimes(1);
115
113
  expect(telemetry.downloadFailed).toHaveBeenCalledWith(
@@ -119,6 +117,8 @@ describe('FileDownloader', () => {
119
117
  revision.claimedSize,
120
118
  );
121
119
  expect(onFinish).toHaveBeenCalledTimes(1);
120
+
121
+ return controller;
122
122
  };
123
123
 
124
124
  const verifyOnProgress = async (downloadedBytes: number[]) => {
@@ -137,8 +137,6 @@ describe('FileDownloader', () => {
137
137
  // @ts-expect-error Mocking WritableStreamDefaultWriter
138
138
  writer = {
139
139
  write: jest.fn(),
140
- close: jest.fn(),
141
- abort: jest.fn(),
142
140
  };
143
141
  // @ts-expect-error Mocking WritableStream
144
142
  stream = {
@@ -338,12 +336,22 @@ describe('FileDownloader', () => {
338
336
  await verifyOnProgress([1, 2, 3]);
339
337
  });
340
338
 
341
- it('should handle failure when verifying manifest', async () => {
339
+ it('should handle failure when verifying manifest with non-recoverable integrity error', async () => {
340
+ cryptoService.verifyManifest = jest.fn().mockImplementation(async function () {
341
+ throw new IntegrityError('Failed to verify manifest');
342
+ });
343
+
344
+ const controller = await verifyFailure('Failed to verify manifest', 6); // All blocks of length 1, 2, 3.
345
+ expect(controller.isDownloadCompleteWithSignatureIssues()).toBe(false);
346
+ });
347
+
348
+ it('should handle failure when verifying manifest with recoverable signature verification error', async () => {
342
349
  cryptoService.verifyManifest = jest.fn().mockImplementation(async function () {
343
- throw new Error('Failed to verify manifest');
350
+ throw new SignatureVerificationError('Failed to verify manifest');
344
351
  });
345
352
 
346
- await verifyFailure('Failed to verify manifest', 6); // All blocks of length 1, 2, 3.
353
+ const controller = await verifyFailure('Failed to verify manifest', 6); // All blocks of length 1, 2, 3.
354
+ expect(controller.isDownloadCompleteWithSignatureIssues()).toBe(true);
347
355
  });
348
356
  });
349
357
 
@@ -389,8 +397,6 @@ describe('FileDownloader', () => {
389
397
  expect(apiService.downloadBlock).toHaveBeenCalledTimes(3);
390
398
  expect(cryptoService.verifyBlockIntegrity).not.toHaveBeenCalled();
391
399
  expect(cryptoService.decryptBlock).toHaveBeenCalledTimes(3);
392
- expect(writer.close).toHaveBeenCalledTimes(1);
393
- expect(writer.abort).not.toHaveBeenCalled();
394
400
  expect(telemetry.downloadFinished).toHaveBeenCalledTimes(1);
395
401
  expect(telemetry.downloadFinished).toHaveBeenCalledWith('revisionUid', 6); // 3 blocks of length 1, 2, 3.
396
402
  expect(telemetry.downloadFailed).not.toHaveBeenCalled();
@@ -1,7 +1,7 @@
1
1
  import { c } from 'ttag';
2
2
 
3
3
  import { PrivateKey, SessionKey, base64StringToUint8Array } from '../../crypto';
4
- import { AbortError } from '../../errors';
4
+ import { AbortError, IntegrityError } from '../../errors';
5
5
  import { Logger } from '../../interface';
6
6
  import { LoggerWithPrefix } from '../../telemetry';
7
7
  import { APIHTTPError, HTTPErrorCode } from '../apiService';
@@ -10,7 +10,7 @@ import { DownloadAPIService } from './apiService';
10
10
  import { getBlockIndex } from './blockIndex';
11
11
  import { DownloadController } from './controller';
12
12
  import { DownloadCryptoService } from './cryptoService';
13
- import { BlockMetadata, RevisionKeys } from './interface';
13
+ import { BlockMetadata, RevisionKeys, SignatureVerificationError } from './interface';
14
14
  import { BufferedSeekableStream } from './seekableStream';
15
15
  import { DownloadTelemetry } from './telemetry';
16
16
 
@@ -233,13 +233,17 @@ export class FileDownloader {
233
233
  );
234
234
  }
235
235
 
236
- await writer.close();
237
236
  void this.telemetry.downloadFinished(this.revision.uid, fileProgress);
238
237
  this.logger.info(`Download succeeded`);
239
238
  } catch (error: unknown) {
240
- this.logger.error(`Download failed`, error);
239
+ if (error instanceof SignatureVerificationError) {
240
+ this.logger.warn(`Download finished with signature verification issues`);
241
+ this.controller.setIsDownloadCompleteWithSignatureIssues(true);
242
+ error = new IntegrityError(error.message, error.debug, { cause: error });
243
+ } else {
244
+ this.logger.error(`Download failed`, error);
245
+ }
241
246
  void this.telemetry.downloadFailed(this.revision.uid, error, fileProgress, this.getClaimedSizeInBytes());
242
- await writer.abort?.();
243
247
  throw error;
244
248
  } finally {
245
249
  this.logger.debug(`Download cleanup`);
@@ -1,4 +1,5 @@
1
1
  import { PrivateKey, PublicKey, SessionKey } from '../../crypto';
2
+ import { IntegrityError } from '../../errors';
2
3
  import { NodeType, Result, MissingNode, MetricVolumeType } from '../../interface';
3
4
  import { DecryptedNode, DecryptedRevision } from '../nodes';
4
5
 
@@ -35,3 +36,17 @@ export interface NodesServiceNode {
35
36
  export interface RevisionsService {
36
37
  getRevision(nodeRevisionUid: string): Promise<DecryptedRevision>;
37
38
  }
39
+
40
+ /**
41
+ * Error thrown when the manifest signature verification fails.
42
+ * This is a special case that is reported as download complete with signature
43
+ * issues. The client must then ask the user to agree to save the file anyway
44
+ * or abort and clean up the file.
45
+ *
46
+ * This error is not exposed to the client. It is only used internally to track
47
+ * the signature verification issues. For the client it must be reported as
48
+ * the IntegrityError.
49
+ */
50
+ export class SignatureVerificationError extends IntegrityError {
51
+ name = 'SignatureVerificationError';
52
+ }
@@ -57,6 +57,9 @@ type PostCopyNodeRequest = Extract<
57
57
  type PostCopyNodeResponse =
58
58
  drivePaths['/drive/volumes/{volumeID}/links/{linkID}/copy']['post']['responses']['200']['content']['application/json'];
59
59
 
60
+ type EmptyTrashResponse =
61
+ drivePaths['/drive/volumes/{volumeID}/trash']['delete']['responses']['200']['content']['application/json'];
62
+
60
63
  type PostTrashNodesRequest = Extract<
61
64
  drivePaths['/drive/v2/volumes/{volumeID}/trash_multiple']['post']['requestBody'],
62
65
  { content: object }
@@ -71,13 +74,20 @@ type PutRestoreNodesRequest = Extract<
71
74
  type PutRestoreNodesResponse =
72
75
  drivePaths['/drive/v2/volumes/{volumeID}/trash/restore_multiple']['put']['responses']['200']['content']['application/json'];
73
76
 
74
- type PostDeleteNodesRequest = Extract<
77
+ type PostDeleteTrashedNodesRequest = Extract<
75
78
  drivePaths['/drive/v2/volumes/{volumeID}/trash/delete_multiple']['post']['requestBody'],
76
79
  { content: object }
77
80
  >['content']['application/json'];
78
- type PostDeleteNodesResponse =
81
+ type PostDeleteTrashedNodesResponse =
79
82
  drivePaths['/drive/v2/volumes/{volumeID}/trash/delete_multiple']['post']['responses']['200']['content']['application/json'];
80
83
 
84
+ type PostDeleteMyNodesRequest = Extract<
85
+ drivePaths['/drive/v2/volumes/{volumeID}/remove-mine']['post']['requestBody'],
86
+ { content: object }
87
+ >['content']['application/json'];
88
+ type PostDeleteMyNodesResponse =
89
+ drivePaths['/drive/v2/volumes/{volumeID}/remove-mine']['post']['responses']['200']['content']['application/json'];
90
+
81
91
  type PostCreateFolderRequest = Extract<
82
92
  drivePaths['/drive/v2/volumes/{volumeID}/folders']['post']['requestBody'],
83
93
  { content: object }
@@ -429,6 +439,10 @@ export abstract class NodeAPIServiceBase<
429
439
  }
430
440
  }
431
441
 
442
+ async emptyTrash(volumeId: string): Promise<void> {
443
+ await this.apiService.delete<EmptyTrashResponse>(`drive/volumes/${volumeId}/trash`);
444
+ }
445
+
432
446
  async *restoreNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
433
447
  for (const { volumeId, batchNodeIds, batchNodeUids } of groupNodeUidsByVolumeAndIteratePerBatch(nodeUids)) {
434
448
  const response = await this.apiService.put<PutRestoreNodesRequest, PutRestoreNodesResponse>(
@@ -446,7 +460,7 @@ export abstract class NodeAPIServiceBase<
446
460
 
447
461
  async *deleteTrashedNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
448
462
  for (const { volumeId, batchNodeIds, batchNodeUids } of groupNodeUidsByVolumeAndIteratePerBatch(nodeUids)) {
449
- const response = await this.apiService.post<PostDeleteNodesRequest, PostDeleteNodesResponse>(
463
+ const response = await this.apiService.post<PostDeleteTrashedNodesRequest, PostDeleteTrashedNodesResponse>(
450
464
  `drive/v2/volumes/${volumeId}/trash/delete_multiple`,
451
465
  {
452
466
  LinkIDs: batchNodeIds,
@@ -459,10 +473,10 @@ export abstract class NodeAPIServiceBase<
459
473
  }
460
474
  }
461
475
 
462
- async *deleteExistingNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
476
+ async *deleteMyNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
463
477
  for (const { volumeId, batchNodeIds, batchNodeUids } of groupNodeUidsByVolumeAndIteratePerBatch(nodeUids)) {
464
- const response = await this.apiService.post<PostDeleteNodesRequest, PostDeleteNodesResponse>(
465
- `drive/v2/volumes/${volumeId}/delete_multiple`,
478
+ const response = await this.apiService.post<PostDeleteMyNodesRequest, PostDeleteMyNodesResponse>(
479
+ `drive/v2/volumes/${volumeId}/remove-mine`,
466
480
  {
467
481
  LinkIDs: batchNodeIds,
468
482
  },