@protontech/drive-sdk 0.9.9 → 0.11.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 (213) hide show
  1. package/dist/crypto/driveCrypto.d.ts +19 -16
  2. package/dist/crypto/driveCrypto.js +23 -1
  3. package/dist/crypto/driveCrypto.js.map +1 -1
  4. package/dist/crypto/driveCrypto.test.js +2 -1
  5. package/dist/crypto/driveCrypto.test.js.map +1 -1
  6. package/dist/crypto/hmac.d.ts +3 -3
  7. package/dist/crypto/hmac.js.map +1 -1
  8. package/dist/crypto/interface.d.ts +45 -25
  9. package/dist/crypto/interface.js.map +1 -1
  10. package/dist/crypto/openPGPCrypto.d.ts +37 -37
  11. package/dist/crypto/openPGPCrypto.js.map +1 -1
  12. package/dist/crypto/utils.d.ts +1 -1
  13. package/dist/diagnostic/telemetry.js +3 -0
  14. package/dist/diagnostic/telemetry.js.map +1 -1
  15. package/dist/interface/index.d.ts +4 -3
  16. package/dist/interface/index.js +3 -1
  17. package/dist/interface/index.js.map +1 -1
  18. package/dist/interface/nodes.d.ts +8 -0
  19. package/dist/interface/photos.d.ts +31 -2
  20. package/dist/interface/photos.js +14 -0
  21. package/dist/interface/photos.js.map +1 -1
  22. package/dist/interface/sharing.d.ts +2 -0
  23. package/dist/interface/telemetry.d.ts +13 -1
  24. package/dist/interface/telemetry.js.map +1 -1
  25. package/dist/interface/thumbnail.d.ts +2 -2
  26. package/dist/internal/apiService/apiService.d.ts +1 -1
  27. package/dist/internal/apiService/apiService.js +27 -14
  28. package/dist/internal/apiService/apiService.js.map +1 -1
  29. package/dist/internal/apiService/apiService.test.js +33 -5
  30. package/dist/internal/apiService/apiService.test.js.map +1 -1
  31. package/dist/internal/apiService/driveTypes.d.ts +2942 -3187
  32. package/dist/internal/apiService/errors.test.js +17 -7
  33. package/dist/internal/apiService/errors.test.js.map +1 -1
  34. package/dist/internal/devices/manager.d.ts +1 -0
  35. package/dist/internal/devices/manager.js +11 -0
  36. package/dist/internal/devices/manager.js.map +1 -1
  37. package/dist/internal/download/apiService.d.ts +1 -1
  38. package/dist/internal/download/cryptoService.d.ts +4 -4
  39. package/dist/internal/download/cryptoService.js.map +1 -1
  40. package/dist/internal/download/fileDownloader.js.map +1 -1
  41. package/dist/internal/download/fileDownloader.test.js.map +1 -1
  42. package/dist/internal/download/thumbnailDownloader.js.map +1 -1
  43. package/dist/internal/nodes/apiService.js.map +1 -1
  44. package/dist/internal/nodes/cryptoService.d.ts +4 -4
  45. package/dist/internal/nodes/cryptoService.js +5 -3
  46. package/dist/internal/nodes/cryptoService.js.map +1 -1
  47. package/dist/internal/nodes/cryptoService.test.js.map +1 -1
  48. package/dist/internal/nodes/interface.d.ts +1 -1
  49. package/dist/internal/nodes/nodesAccess.js +1 -1
  50. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  51. package/dist/internal/nodes/nodesManagement.js +0 -1
  52. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  53. package/dist/internal/photos/addToAlbum.d.ts +42 -0
  54. package/dist/internal/photos/addToAlbum.js +178 -0
  55. package/dist/internal/photos/addToAlbum.js.map +1 -0
  56. package/dist/internal/photos/addToAlbum.test.js +409 -0
  57. package/dist/internal/photos/addToAlbum.test.js.map +1 -0
  58. package/dist/internal/photos/albumsCrypto.d.ts +20 -3
  59. package/dist/internal/photos/albumsCrypto.js +27 -0
  60. package/dist/internal/photos/albumsCrypto.js.map +1 -1
  61. package/dist/internal/photos/{albums.d.ts → albumsManager.d.ts} +6 -4
  62. package/dist/internal/photos/{albums.js → albumsManager.js} +17 -5
  63. package/dist/internal/photos/albumsManager.js.map +1 -0
  64. package/dist/internal/photos/albumsManager.test.d.ts +1 -0
  65. package/dist/internal/photos/{albums.test.js → albumsManager.test.js} +4 -3
  66. package/dist/internal/photos/albumsManager.test.js.map +1 -0
  67. package/dist/internal/photos/apiService.d.ts +22 -2
  68. package/dist/internal/photos/apiService.js +136 -5
  69. package/dist/internal/photos/apiService.js.map +1 -1
  70. package/dist/internal/photos/apiService.test.d.ts +1 -0
  71. package/dist/internal/photos/apiService.test.js +199 -0
  72. package/dist/internal/photos/apiService.test.js.map +1 -0
  73. package/dist/internal/photos/errors.d.ts +4 -0
  74. package/dist/internal/photos/errors.js +17 -0
  75. package/dist/internal/photos/errors.js.map +1 -0
  76. package/dist/internal/photos/index.d.ts +5 -3
  77. package/dist/internal/photos/index.js +5 -2
  78. package/dist/internal/photos/index.js.map +1 -1
  79. package/dist/internal/photos/interface.d.ts +6 -15
  80. package/dist/internal/photos/interface.js +0 -14
  81. package/dist/internal/photos/interface.js.map +1 -1
  82. package/dist/internal/photos/nodes.js +33 -3
  83. package/dist/internal/photos/nodes.js.map +1 -1
  84. package/dist/internal/photos/nodes.test.js +25 -5
  85. package/dist/internal/photos/nodes.test.js.map +1 -1
  86. package/dist/internal/photos/photosManager.d.ts +22 -0
  87. package/dist/internal/photos/photosManager.js +101 -0
  88. package/dist/internal/photos/photosManager.js.map +1 -0
  89. package/dist/internal/photos/photosManager.test.d.ts +1 -0
  90. package/dist/internal/photos/photosManager.test.js +222 -0
  91. package/dist/internal/photos/photosManager.test.js.map +1 -0
  92. package/dist/internal/photos/photosTransferPayloadBuilder.d.ts +57 -0
  93. package/dist/internal/photos/photosTransferPayloadBuilder.js +113 -0
  94. package/dist/internal/photos/photosTransferPayloadBuilder.js.map +1 -0
  95. package/dist/internal/photos/photosTransferPayloadBuilder.test.d.ts +1 -0
  96. package/dist/internal/photos/photosTransferPayloadBuilder.test.js +289 -0
  97. package/dist/internal/photos/photosTransferPayloadBuilder.test.js.map +1 -0
  98. package/dist/internal/photos/upload.d.ts +2 -2
  99. package/dist/internal/photos/upload.js +1 -1
  100. package/dist/internal/photos/upload.js.map +1 -1
  101. package/dist/internal/shares/apiService.js +1 -0
  102. package/dist/internal/shares/apiService.js.map +1 -1
  103. package/dist/internal/shares/interface.d.ts +1 -0
  104. package/dist/internal/sharing/apiService.d.ts +8 -1
  105. package/dist/internal/sharing/apiService.js +23 -1
  106. package/dist/internal/sharing/apiService.js.map +1 -1
  107. package/dist/internal/sharing/cryptoService.js +8 -4
  108. package/dist/internal/sharing/cryptoService.js.map +1 -1
  109. package/dist/internal/sharing/sharingManagement.d.ts +1 -0
  110. package/dist/internal/sharing/sharingManagement.js +15 -2
  111. package/dist/internal/sharing/sharingManagement.js.map +1 -1
  112. package/dist/internal/sharing/sharingManagement.test.js +30 -5
  113. package/dist/internal/sharing/sharingManagement.test.js.map +1 -1
  114. package/dist/internal/sharingPublic/nodes.d.ts +2 -2
  115. package/dist/internal/sharingPublic/nodes.js.map +1 -1
  116. package/dist/internal/upload/apiService.d.ts +3 -7
  117. package/dist/internal/upload/apiService.js +0 -4
  118. package/dist/internal/upload/apiService.js.map +1 -1
  119. package/dist/internal/upload/blockVerifier.d.ts +2 -2
  120. package/dist/internal/upload/blockVerifier.js.map +1 -1
  121. package/dist/internal/upload/chunkStreamReader.d.ts +2 -2
  122. package/dist/internal/upload/chunkStreamReader.js.map +1 -1
  123. package/dist/internal/upload/chunkStreamReader.test.js.map +1 -1
  124. package/dist/internal/upload/cryptoService.d.ts +7 -7
  125. package/dist/internal/upload/cryptoService.js +4 -4
  126. package/dist/internal/upload/cryptoService.js.map +1 -1
  127. package/dist/internal/upload/interface.d.ts +6 -6
  128. package/dist/internal/upload/manager.d.ts +1 -1
  129. package/dist/internal/upload/manager.js.map +1 -1
  130. package/dist/internal/upload/streamUploader.d.ts +1 -1
  131. package/dist/internal/upload/streamUploader.js +12 -13
  132. package/dist/internal/upload/streamUploader.js.map +1 -1
  133. package/dist/internal/upload/streamUploader.test.js +28 -4
  134. package/dist/internal/upload/streamUploader.test.js.map +1 -1
  135. package/dist/internal/utils.d.ts +1 -1
  136. package/dist/protonDriveClient.d.ts +8 -0
  137. package/dist/protonDriveClient.js +12 -1
  138. package/dist/protonDriveClient.js.map +1 -1
  139. package/dist/protonDrivePhotosClient.d.ts +35 -3
  140. package/dist/protonDrivePhotosClient.js +42 -2
  141. package/dist/protonDrivePhotosClient.js.map +1 -1
  142. package/dist/protonDrivePublicLinkClient.js +1 -1
  143. package/dist/protonDrivePublicLinkClient.js.map +1 -1
  144. package/dist/transformers.js +2 -0
  145. package/dist/transformers.js.map +1 -1
  146. package/package.json +4 -4
  147. package/src/crypto/driveCrypto.test.ts +2 -1
  148. package/src/crypto/driveCrypto.ts +50 -16
  149. package/src/crypto/hmac.ts +4 -4
  150. package/src/crypto/interface.ts +58 -27
  151. package/src/crypto/openPGPCrypto.ts +26 -26
  152. package/src/diagnostic/telemetry.ts +3 -0
  153. package/src/interface/index.ts +11 -2
  154. package/src/interface/nodes.ts +1 -0
  155. package/src/interface/photos.ts +33 -2
  156. package/src/interface/sharing.ts +2 -0
  157. package/src/interface/telemetry.ts +15 -1
  158. package/src/interface/thumbnail.ts +2 -2
  159. package/src/internal/apiService/apiService.test.ts +38 -6
  160. package/src/internal/apiService/apiService.ts +39 -14
  161. package/src/internal/apiService/driveTypes.ts +2942 -3187
  162. package/src/internal/devices/manager.ts +14 -0
  163. package/src/internal/download/apiService.ts +1 -1
  164. package/src/internal/download/cryptoService.ts +4 -4
  165. package/src/internal/download/fileDownloader.test.ts +4 -4
  166. package/src/internal/download/fileDownloader.ts +6 -6
  167. package/src/internal/download/thumbnailDownloader.ts +4 -4
  168. package/src/internal/nodes/apiService.ts +2 -2
  169. package/src/internal/nodes/cryptoService.test.ts +2 -2
  170. package/src/internal/nodes/cryptoService.ts +11 -8
  171. package/src/internal/nodes/interface.ts +1 -1
  172. package/src/internal/nodes/nodesAccess.ts +1 -1
  173. package/src/internal/nodes/nodesManagement.ts +0 -1
  174. package/src/internal/photos/addToAlbum.test.ts +515 -0
  175. package/src/internal/photos/addToAlbum.ts +234 -0
  176. package/src/internal/photos/albumsCrypto.ts +54 -3
  177. package/src/internal/photos/{albums.test.ts → albumsManager.test.ts} +22 -25
  178. package/src/internal/photos/{albums.ts → albumsManager.ts} +32 -3
  179. package/src/internal/photos/apiService.test.ts +233 -0
  180. package/src/internal/photos/apiService.ts +228 -26
  181. package/src/internal/photos/errors.ts +11 -0
  182. package/src/internal/photos/index.ts +6 -3
  183. package/src/internal/photos/interface.ts +8 -18
  184. package/src/internal/photos/nodes.test.ts +27 -6
  185. package/src/internal/photos/nodes.ts +35 -3
  186. package/src/internal/photos/photosManager.test.ts +266 -0
  187. package/src/internal/photos/photosManager.ts +144 -0
  188. package/src/internal/photos/photosTransferPayloadBuilder.test.ts +380 -0
  189. package/src/internal/photos/photosTransferPayloadBuilder.ts +203 -0
  190. package/src/internal/photos/upload.ts +8 -3
  191. package/src/internal/shares/apiService.ts +1 -0
  192. package/src/internal/shares/interface.ts +1 -0
  193. package/src/internal/sharing/apiService.ts +49 -5
  194. package/src/internal/sharing/cryptoService.ts +10 -4
  195. package/src/internal/sharing/sharingManagement.test.ts +33 -5
  196. package/src/internal/sharing/sharingManagement.ts +28 -6
  197. package/src/internal/sharingPublic/nodes.ts +1 -1
  198. package/src/internal/upload/apiService.ts +10 -12
  199. package/src/internal/upload/blockVerifier.ts +3 -3
  200. package/src/internal/upload/chunkStreamReader.test.ts +7 -7
  201. package/src/internal/upload/chunkStreamReader.ts +3 -3
  202. package/src/internal/upload/cryptoService.ts +11 -12
  203. package/src/internal/upload/interface.ts +6 -6
  204. package/src/internal/upload/manager.ts +2 -2
  205. package/src/internal/upload/streamUploader.test.ts +33 -4
  206. package/src/internal/upload/streamUploader.ts +13 -13
  207. package/src/protonDriveClient.ts +16 -4
  208. package/src/protonDrivePhotosClient.ts +73 -17
  209. package/src/protonDrivePublicLinkClient.ts +1 -1
  210. package/src/transformers.ts +2 -0
  211. package/dist/internal/photos/albums.js.map +0 -1
  212. package/dist/internal/photos/albums.test.js.map +0 -1
  213. /package/dist/internal/photos/{albums.test.d.ts → addToAlbum.test.d.ts} +0 -0
@@ -36,6 +36,9 @@ type GetSharedNodesResponse =
36
36
  type GetSharedWithMeNodesResponse =
37
37
  drivePaths['/drive/v2/sharedwithme']['get']['responses']['200']['content']['application/json'];
38
38
 
39
+ type GetSharedAlbumsResponse =
40
+ drivePaths['/drive/photos/albums/shared-with-me']['get']['responses']['200']['content']['application/json'];
41
+
39
42
  type GetInvitationsResponse =
40
43
  drivePaths['/drive/v2/shares/invitations']['get']['responses']['200']['content']['application/json'];
41
44
 
@@ -61,8 +64,10 @@ type GetShareExternalInvitations =
61
64
  type GetShareMembers =
62
65
  drivePaths['/drive/v2/shares/{shareID}/members']['get']['responses']['200']['content']['application/json'];
63
66
 
64
- type PostSharedBookmarksRequest =
65
- Extract<drivePaths['/drive/v2/urls/{token}/bookmark']['post']['requestBody'], {content: object}>['content']['application/json'];
67
+ type PostSharedBookmarksRequest = Extract<
68
+ drivePaths['/drive/v2/urls/{token}/bookmark']['post']['requestBody'],
69
+ { content: object }
70
+ >['content']['application/json'];
66
71
  type PostSharedBookmarksResponse =
67
72
  drivePaths['/drive/v2/urls/{token}/bookmark']['post']['responses']['200']['content']['application/json'];
68
73
 
@@ -73,6 +78,14 @@ type PostCreateShareRequest = Extract<
73
78
  type PostCreateShareResponse =
74
79
  drivePaths['/drive/volumes/{volumeID}/shares']['post']['responses']['200']['content']['application/json'];
75
80
 
81
+ type PostChangeSharePropertiesRequest = Extract<
82
+ drivePaths['/drive/shares/{shareID}/property']['post']['requestBody'],
83
+ { content: object }
84
+ >['content']['application/json'];
85
+
86
+ type PostChangeSharePropertiesResponse =
87
+ drivePaths['/drive/shares/{shareID}/property']['post']['responses']['200']['content']['application/json'];
88
+
76
89
  type PostInviteProtonUserRequest = Extract<
77
90
  drivePaths['/drive/v2/shares/{shareID}/invitations']['post']['requestBody'],
78
91
  { content: object }
@@ -184,6 +197,30 @@ export class SharingAPIService {
184
197
  }
185
198
  anchor = response.AnchorID;
186
199
  }
200
+
201
+ if (this.shareTargetTypes.includes(ShareTargetType.Album)) {
202
+ yield* this.iterateSharedAlbumUids(signal);
203
+ }
204
+ }
205
+
206
+ // TODO: Sharing cannot know about albums. We should remove this and use
207
+ // ShareTargetTypes when it is supported by the API.
208
+ private async *iterateSharedAlbumUids(signal?: AbortSignal): AsyncGenerator<string> {
209
+ let anchor = '';
210
+ while (true) {
211
+ const response = await this.apiService.get<GetSharedAlbumsResponse>(
212
+ `drive/photos/albums/shared-with-me?${anchor ? `AnchorID=${anchor}` : ''}`,
213
+ signal,
214
+ );
215
+ for (const album of response.Albums) {
216
+ yield makeNodeUid(album.VolumeID, album.LinkID);
217
+ }
218
+
219
+ if (!response.More || !response.AnchorID) {
220
+ break;
221
+ }
222
+ anchor = response.AnchorID;
223
+ }
187
224
  }
188
225
 
189
226
  async *iterateInvitationUids(signal?: AbortSignal): AsyncGenerator<string> {
@@ -304,7 +341,7 @@ export class SharingAPIService {
304
341
  addressId: string;
305
342
  addressKeyId: string;
306
343
  }): Promise<void> {
307
- await this.apiService.post<PostSharedBookmarksRequest, PostSharedBookmarksResponse>(
344
+ await this.apiService.post<PostSharedBookmarksRequest, PostSharedBookmarksResponse>(
308
345
  `drive/v2/urls/${bookmark.token}/bookmark`,
309
346
  {
310
347
  BookmarkShareURL: {
@@ -363,7 +400,7 @@ export class SharingAPIService {
363
400
  base64PassphraseKeyPacket: string;
364
401
  base64NameKeyPacket: string;
365
402
  },
366
- ): Promise<string> {
403
+ ): Promise<{ shareId: string; editorsCanShare: boolean }> {
367
404
  const { volumeId, nodeId } = splitNodeUid(nodeUid);
368
405
  const response = await this.apiService.post<PostCreateShareRequest, PostCreateShareResponse>(
369
406
  `drive/volumes/${volumeId}/shares`,
@@ -378,13 +415,20 @@ export class SharingAPIService {
378
415
  NameKeyPacket: node.base64NameKeyPacket,
379
416
  },
380
417
  );
381
- return response.Share.ID;
418
+ return { shareId: response.Share.ID, editorsCanShare: response.Share.EditorsCanShare };
382
419
  }
383
420
 
384
421
  async deleteShare(shareId: string, force: boolean = false): Promise<void> {
385
422
  await this.apiService.delete(`drive/shares/${shareId}?Force=${force ? 1 : 0}`);
386
423
  }
387
424
 
425
+ async changeShareProperties(shareId: string, { editorsCanShare }: { editorsCanShare: boolean }) {
426
+ await this.apiService.post<PostChangeSharePropertiesRequest, PostChangeSharePropertiesResponse>(
427
+ `drive/shares/${shareId}/property`,
428
+ { EditorsCanShare: editorsCanShare },
429
+ );
430
+ }
431
+
388
432
  async inviteProtonUser(
389
433
  shareId: string,
390
434
  invitation: EncryptedInvitationRequest,
@@ -213,7 +213,7 @@ export class SharingCryptoService {
213
213
  } catch (error: unknown) {
214
214
  const message = getErrorMessage(error);
215
215
  const errorMessage = c('Error').t`Failed to decrypt item name: ${message}`;
216
- nodeName = resultError(new Error(errorMessage));
216
+ nodeName = resultError(new Error(errorMessage, { cause: error }));
217
217
  }
218
218
 
219
219
  return {
@@ -458,7 +458,10 @@ export class SharingCryptoService {
458
458
  urlPassword = result.password;
459
459
  customPassword = resultOk(result.customPassword);
460
460
  } catch (originalError: unknown) {
461
- const error = originalError instanceof Error ? originalError : new Error(c('Error').t`Unknown error`);
461
+ const error =
462
+ originalError instanceof Error
463
+ ? originalError
464
+ : new Error(c('Error').t`Unknown error`, { cause: originalError });
462
465
  return {
463
466
  url: resultError(error),
464
467
  customPassword: resultError(error),
@@ -473,7 +476,10 @@ export class SharingCryptoService {
473
476
  try {
474
477
  shareKey = await this.decryptBookmarkKey(encryptedBookmark, password);
475
478
  } catch (originalError: unknown) {
476
- const error = originalError instanceof Error ? originalError : new Error(c('Error').t`Unknown error`);
479
+ const error =
480
+ originalError instanceof Error
481
+ ? originalError
482
+ : new Error(c('Error').t`Unknown error`, { cause: originalError });
477
483
  return {
478
484
  url,
479
485
  customPassword,
@@ -577,7 +583,7 @@ export class SharingCryptoService {
577
583
 
578
584
  const message = getErrorMessage(error);
579
585
  const errorMessage = c('Error').t`Failed to decrypt bookmark name: ${message}`;
580
- return resultError(new Error(errorMessage));
586
+ return resultError(new Error(errorMessage, { cause: error }));
581
587
  }
582
588
  }
583
589
  }
@@ -18,6 +18,8 @@ import { SharingManagement } from './sharingManagement';
18
18
  import { ValidationError } from '../../errors';
19
19
  import { ErrorCode } from '../apiService';
20
20
 
21
+ const DEFAULT_SHARE_ID = 'shareId';
22
+
21
23
  describe('SharingManagement', () => {
22
24
  let logger: Logger;
23
25
  let apiService: SharingAPIService;
@@ -34,7 +36,7 @@ describe('SharingManagement', () => {
34
36
 
35
37
  // @ts-expect-error No need to implement all methods for mocking
36
38
  apiService = {
37
- createStandardShare: jest.fn().mockReturnValue('newShareId'),
39
+ createStandardShare: jest.fn().mockReturnValue({ shareId: 'newShareId', editorsCanShare: false }),
38
40
  getShareInvitations: jest.fn().mockResolvedValue([]),
39
41
  getShareExternalInvitations: jest.fn().mockResolvedValue([]),
40
42
  getShareMembers: jest.fn().mockResolvedValue([]),
@@ -63,6 +65,7 @@ describe('SharingManagement', () => {
63
65
  publicUrl: 'publicLinkUrl',
64
66
  }),
65
67
  updatePublicLink: jest.fn(),
68
+ changeShareProperties: jest.fn(),
66
69
  };
67
70
  // @ts-expect-error No need to implement all methods for mocking
68
71
  cache = {
@@ -98,7 +101,7 @@ describe('SharingManagement', () => {
98
101
  // @ts-expect-error No need to implement all methods for mocking
99
102
  sharesService = {
100
103
  loadEncryptedShare: jest.fn().mockResolvedValue({
101
- id: 'shareId',
104
+ id: DEFAULT_SHARE_ID,
102
105
  addressId: 'addressId',
103
106
  creatorEmail: 'address@example.com',
104
107
  passphraseSessionKey: 'sharePassphraseSessionKey',
@@ -106,9 +109,11 @@ describe('SharingManagement', () => {
106
109
  };
107
110
  // @ts-expect-error No need to implement all methods for mocking
108
111
  nodesService = {
109
- getNode: jest
110
- .fn()
111
- .mockImplementation((nodeUid) => ({ nodeUid, shareId: 'shareId', name: { ok: true, value: 'name' } })),
112
+ getNode: jest.fn().mockImplementation((nodeUid) => ({
113
+ nodeUid,
114
+ shareId: DEFAULT_SHARE_ID,
115
+ name: { ok: true, value: 'name' },
116
+ })),
112
117
  getNodeKeys: jest.fn().mockImplementation((nodeUid) => ({ key: 'node-key' })),
113
118
  getNodePrivateAndSessionKeys: jest.fn().mockImplementation((nodeUid) => ({})),
114
119
  getRootNodeEmailKey: jest.fn().mockResolvedValue({ email: 'volume-email', addressKey: 'volume-key' }),
@@ -225,6 +230,7 @@ describe('SharingManagement', () => {
225
230
  nonProtonInvitations: [],
226
231
  members: [],
227
232
  publicLink: undefined,
233
+ editorsCanShare: false,
228
234
  });
229
235
  expect(apiService.updateInvitation).not.toHaveBeenCalled();
230
236
  expect(apiService.inviteProtonUser).toHaveBeenCalled();
@@ -395,6 +401,28 @@ describe('SharingManagement', () => {
395
401
  expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
396
402
  });
397
403
 
404
+ it('should update editorsCanChange', async () => {
405
+ const sharingInfo = await sharingManagement.shareNode(nodeUid, {
406
+ editorsCanShare: true,
407
+ });
408
+
409
+ expect(sharingInfo).toEqual({
410
+ protonInvitations: [
411
+ {
412
+ ...invitation,
413
+ role: 'viewer',
414
+ },
415
+ ],
416
+ nonProtonInvitations: [externalInvitation],
417
+ members: [member],
418
+ publicLink: undefined,
419
+ editorsCanShare: true,
420
+ });
421
+ expect(apiService.changeShareProperties).toHaveBeenCalledWith(DEFAULT_SHARE_ID, {
422
+ editorsCanShare: true,
423
+ });
424
+ });
425
+
398
426
  it('should be no-op if no change', async () => {
399
427
  const sharingInfo = await sharingManagement.shareNode(nodeUid, {
400
428
  users: [{ email: 'internal-email', role: MemberRole.Viewer }],
@@ -77,11 +77,12 @@ export class SharingManagement {
77
77
  return;
78
78
  }
79
79
 
80
- const [protonInvitations, nonProtonInvitations, members, publicLink] = await Promise.all([
80
+ const [protonInvitations, nonProtonInvitations, members, publicLink, share] = await Promise.all([
81
81
  Array.fromAsync(this.iterateShareInvitations(node.shareId)),
82
82
  Array.fromAsync(this.iterateShareExternalInvitations(node.shareId)),
83
83
  Array.fromAsync(this.iterateShareMembers(node.shareId)),
84
84
  this.getPublicLink(node.shareId),
85
+ this.sharesService.loadEncryptedShare(node.shareId),
85
86
  ]);
86
87
 
87
88
  return {
@@ -89,6 +90,7 @@ export class SharingManagement {
89
90
  nonProtonInvitations,
90
91
  members,
91
92
  publicLink,
93
+ editorsCanShare: share.editorsCanShare,
92
94
  };
93
95
  }
94
96
 
@@ -161,6 +163,7 @@ export class SharingManagement {
161
163
  nonProtonInvitations: [],
162
164
  members: [],
163
165
  publicLink: undefined,
166
+ editorsCanShare: result.editorsCanShare,
164
167
  };
165
168
  contextShareAddress = result.contextShareAddress;
166
169
  } catch (error: unknown) {
@@ -184,6 +187,11 @@ export class SharingManagement {
184
187
  contextShareAddress = await this.nodesService.getRootNodeEmailKey(nodeUid);
185
188
  }
186
189
 
190
+ if (settings.editorsCanShare !== undefined) {
191
+ await this.setEditorsCanShare(currentSharing.share.shareId, settings.editorsCanShare);
192
+ currentSharing.editorsCanShare = settings.editorsCanShare;
193
+ }
194
+
187
195
  const emailOptions: EmailOptions = {
188
196
  message: settings.emailOptions?.message,
189
197
  nodeName: settings.emailOptions?.includeNodeName ? currentSharing.nodeName : undefined,
@@ -294,6 +302,7 @@ export class SharingManagement {
294
302
  nonProtonInvitations: currentSharing.nonProtonInvitations,
295
303
  members: currentSharing.members,
296
304
  publicLink: currentSharing.publicLink,
305
+ editorsCanShare: currentSharing.editorsCanShare,
297
306
  };
298
307
  }
299
308
 
@@ -385,6 +394,7 @@ export class SharingManagement {
385
394
  nonProtonInvitations: currentSharing.nonProtonInvitations,
386
395
  members: currentSharing.members,
387
396
  publicLink: currentSharing.publicLink,
397
+ editorsCanShare: currentSharing.editorsCanShare,
388
398
  };
389
399
  }
390
400
 
@@ -415,7 +425,9 @@ export class SharingManagement {
415
425
  };
416
426
  }
417
427
 
418
- private async createShare(nodeUid: string): Promise<{ share: Share; contextShareAddress: ContextShareAddress }> {
428
+ private async createShare(
429
+ nodeUid: string,
430
+ ): Promise<{ share: Share; contextShareAddress: ContextShareAddress; editorsCanShare: boolean }> {
419
431
  const node = await this.nodesService.getNode(nodeUid);
420
432
  if (!node.parentUid) {
421
433
  throw new ValidationError(c('Error').t`Cannot share root folder`);
@@ -426,10 +438,15 @@ export class SharingManagement {
426
438
 
427
439
  const nodeKeys = await this.nodesService.getNodePrivateAndSessionKeys(nodeUid);
428
440
  const keys = await this.cryptoService.generateShareKeys(nodeKeys, addressKey);
429
- const shareId = await this.apiService.createStandardShare(nodeUid, addressId, keys.shareKey.encrypted, {
430
- base64PassphraseKeyPacket: keys.base64PpassphraseKeyPacket,
431
- base64NameKeyPacket: keys.base64NameKeyPacket,
432
- });
441
+ const { shareId, editorsCanShare } = await this.apiService.createStandardShare(
442
+ nodeUid,
443
+ addressId,
444
+ keys.shareKey.encrypted,
445
+ {
446
+ base64PassphraseKeyPacket: keys.base64PpassphraseKeyPacket,
447
+ base64NameKeyPacket: keys.base64NameKeyPacket,
448
+ },
449
+ );
433
450
  await this.nodesService.notifyNodeChanged(nodeUid);
434
451
  if (await this.cache.hasSharedByMeNodeUidsLoaded()) {
435
452
  await this.cache.addSharedByMeNodeUid(nodeUid);
@@ -449,9 +466,14 @@ export class SharingManagement {
449
466
  return {
450
467
  share,
451
468
  contextShareAddress,
469
+ editorsCanShare,
452
470
  };
453
471
  }
454
472
 
473
+ private async setEditorsCanShare(shareId: string, editorsCanShare: boolean) {
474
+ await this.apiService.changeShareProperties(shareId, { editorsCanShare });
475
+ }
476
+
455
477
  /**
456
478
  * Deletes the share even if it is not empty.
457
479
  */
@@ -18,7 +18,7 @@ import { SharingPublicSharesManager } from './shares';
18
18
 
19
19
  export class SharingPublicNodesCryptoService extends NodesCryptoService {
20
20
  async generateDocument(
21
- parentKeys: { key: PrivateKey; hashKey: Uint8Array },
21
+ parentKeys: { key: PrivateKey; hashKey: Uint8Array<ArrayBuffer> },
22
22
  signingKeys: NodeSigningKeys,
23
23
  name: string,
24
24
  ) {
@@ -142,7 +142,7 @@ export class UploadAPIService {
142
142
  }
143
143
 
144
144
  async getVerificationData(draftNodeRevisionUid: string): Promise<{
145
- verificationCode: Uint8Array;
145
+ verificationCode: Uint8Array<ArrayBuffer>;
146
146
  base64ContentKeyPacket: string;
147
147
  }> {
148
148
  const { volumeId, nodeId, revisionId } = splitNodeRevisionUid(draftNodeRevisionUid);
@@ -162,22 +162,24 @@ export class UploadAPIService {
162
162
  blocks: {
163
163
  contentBlocks: {
164
164
  index: number;
165
- hash: Uint8Array;
166
- encryptedSize: number;
167
165
  armoredSignature: string;
168
- verificationToken: Uint8Array;
166
+ verificationToken: Uint8Array<ArrayBuffer>;
169
167
  }[];
170
168
  thumbnails?: {
171
169
  type: ThumbnailType;
172
- hash: Uint8Array;
173
- encryptedSize: number;
174
170
  }[];
175
171
  },
176
172
  ): Promise<UploadTokens> {
177
173
  const { volumeId, nodeId, revisionId } = splitNodeRevisionUid(draftNodeRevisionUid);
178
174
  const result = await this.apiService.post<
179
175
  // TODO: Deprected fields but not properly marked in the types.
180
- Omit<PostRequestBlockUploadRequest, 'ShareID' | 'Thumbnail' | 'ThumbnailHash' | 'ThumbnailSize'>,
176
+ Omit<
177
+ PostRequestBlockUploadRequest,
178
+ 'ShareID' | 'Thumbnail' | 'ThumbnailHash' | 'ThumbnailSize' | 'BlockList' | 'ThumbnailList'
179
+ > & {
180
+ BlockList: Omit<PostRequestBlockUploadRequest['BlockList'][0], 'Hash' | 'Size'>[];
181
+ ThumbnailList: Omit<PostRequestBlockUploadRequest['ThumbnailList'][0], 'Hash' | 'Size'>[];
182
+ },
181
183
  PostRequestBlockUploadResponse
182
184
  >('drive/blocks', {
183
185
  AddressID: addressId,
@@ -186,16 +188,12 @@ export class UploadAPIService {
186
188
  RevisionID: revisionId,
187
189
  BlockList: blocks.contentBlocks.map((block) => ({
188
190
  Index: block.index,
189
- Hash: uint8ArrayToBase64String(block.hash),
190
191
  EncSignature: block.armoredSignature,
191
- Size: block.encryptedSize,
192
192
  Verifier: {
193
193
  Token: uint8ArrayToBase64String(block.verificationToken),
194
194
  },
195
195
  })),
196
196
  ThumbnailList: (blocks.thumbnails || []).map((block) => ({
197
- Hash: uint8ArrayToBase64String(block.hash),
198
- Size: block.encryptedSize,
199
197
  Type: block.type,
200
198
  })),
201
199
  });
@@ -260,7 +258,7 @@ export class UploadAPIService {
260
258
  async uploadBlock(
261
259
  url: string,
262
260
  token: string,
263
- block: Uint8Array,
261
+ block: Uint8Array<ArrayBuffer>,
264
262
  onProgress?: (uploadedBytes: number) => void,
265
263
  signal?: AbortSignal,
266
264
  ): Promise<void> {
@@ -3,7 +3,7 @@ import { UploadAPIService } from './apiService';
3
3
  import { UploadCryptoService } from './cryptoService';
4
4
 
5
5
  export class BlockVerifier {
6
- private verificationCode?: Uint8Array;
6
+ private verificationCode?: Uint8Array<ArrayBuffer>;
7
7
  private contentKeyPacketSessionKey?: SessionKey;
8
8
 
9
9
  constructor(
@@ -26,8 +26,8 @@ export class BlockVerifier {
26
26
  );
27
27
  }
28
28
 
29
- async verifyBlock(encryptedBlock: Uint8Array): Promise<{
30
- verificationToken: Uint8Array;
29
+ async verifyBlock(encryptedBlock: Uint8Array<ArrayBuffer>): Promise<{
30
+ verificationToken: Uint8Array<ArrayBuffer>;
31
31
  }> {
32
32
  if (!this.verificationCode || !this.contentKeyPacketSessionKey) {
33
33
  throw new Error('Verifying block before loading verification data');
@@ -1,10 +1,10 @@
1
1
  import { ChunkStreamReader } from './chunkStreamReader';
2
2
 
3
3
  describe('ChunkStreamReader', () => {
4
- let stream: ReadableStream<Uint8Array>;
4
+ let stream: ReadableStream<Uint8Array<ArrayBuffer>>;
5
5
 
6
6
  beforeEach(() => {
7
- stream = new ReadableStream<Uint8Array>({
7
+ stream = new ReadableStream<Uint8Array<ArrayBuffer>>({
8
8
  start(controller) {
9
9
  controller.enqueue(new Uint8Array([1, 2, 3]));
10
10
  controller.enqueue(new Uint8Array([4, 5, 6]));
@@ -18,7 +18,7 @@ describe('ChunkStreamReader', () => {
18
18
  it('should yield chunks as enqueued if matching the size', async () => {
19
19
  const reader = new ChunkStreamReader(stream, 3);
20
20
 
21
- const chunks: Uint8Array[] = [];
21
+ const chunks: Uint8Array<ArrayBuffer>[] = [];
22
22
  for await (const chunk of reader.iterateChunks()) {
23
23
  chunks.push(new Uint8Array(chunk));
24
24
  }
@@ -33,7 +33,7 @@ describe('ChunkStreamReader', () => {
33
33
  it('should yield smaller chunks than enqueued chunks', async () => {
34
34
  const reader = new ChunkStreamReader(stream, 2);
35
35
 
36
- const chunks: Uint8Array[] = [];
36
+ const chunks: Uint8Array<ArrayBuffer>[] = [];
37
37
  for await (const chunk of reader.iterateChunks()) {
38
38
  chunks.push(new Uint8Array(chunk));
39
39
  }
@@ -50,7 +50,7 @@ describe('ChunkStreamReader', () => {
50
50
  it('should yield bigger chunks than enqueued chunks', async () => {
51
51
  const reader = new ChunkStreamReader(stream, 4);
52
52
 
53
- const chunks: Uint8Array[] = [];
53
+ const chunks: Uint8Array<ArrayBuffer>[] = [];
54
54
  for await (const chunk of reader.iterateChunks()) {
55
55
  chunks.push(new Uint8Array(chunk));
56
56
  }
@@ -64,7 +64,7 @@ describe('ChunkStreamReader', () => {
64
64
  it('should yield last incomplete chunk', async () => {
65
65
  const reader = new ChunkStreamReader(stream, 5);
66
66
 
67
- const chunks: Uint8Array[] = [];
67
+ const chunks: Uint8Array<ArrayBuffer>[] = [];
68
68
  for await (const chunk of reader.iterateChunks()) {
69
69
  chunks.push(new Uint8Array(chunk));
70
70
  }
@@ -78,7 +78,7 @@ describe('ChunkStreamReader', () => {
78
78
  it('should yield as one big chunk', async () => {
79
79
  const reader = new ChunkStreamReader(stream, 100);
80
80
 
81
- const chunks: Uint8Array[] = [];
81
+ const chunks: Uint8Array<ArrayBuffer>[] = [];
82
82
  for await (const chunk of reader.iterateChunks()) {
83
83
  chunks.push(new Uint8Array(chunk));
84
84
  }
@@ -6,16 +6,16 @@
6
6
  * If you need to keep previous chunks, copy them to a new array.
7
7
  */
8
8
  export class ChunkStreamReader {
9
- private reader: ReadableStreamDefaultReader<Uint8Array>;
9
+ private reader: ReadableStreamDefaultReader<Uint8Array<ArrayBuffer>>;
10
10
 
11
11
  private chunkSize: number;
12
12
 
13
- constructor(stream: ReadableStream<Uint8Array>, chunkSize: number) {
13
+ constructor(stream: ReadableStream<Uint8Array<ArrayBuffer>>, chunkSize: number) {
14
14
  this.reader = stream.getReader();
15
15
  this.chunkSize = chunkSize;
16
16
  }
17
17
 
18
- async *iterateChunks(): AsyncGenerator<Uint8Array> {
18
+ async *iterateChunks(): AsyncGenerator<Uint8Array<ArrayBuffer>> {
19
19
  const buffer = new Uint8Array(this.chunkSize);
20
20
 
21
21
  let position = 0;
@@ -23,7 +23,7 @@ export class UploadCryptoService {
23
23
 
24
24
  async generateFileCrypto(
25
25
  parentUid: string,
26
- parentKeys: { key: PrivateKey; hashKey: Uint8Array },
26
+ parentKeys: { key: PrivateKey; hashKey: Uint8Array<ArrayBuffer> },
27
27
  name: string,
28
28
  ): Promise<NodeCrypto> {
29
29
  const signingKeys = await this.getSigningKeys({ parentNodeUid: parentUid });
@@ -111,21 +111,21 @@ export class UploadCryptoService {
111
111
  nodeRevisionDraftKeys.signingKeys.contentSigningKey,
112
112
  );
113
113
 
114
- const digest = await crypto.subtle.digest('SHA-256', encryptedData);
114
+ const digestPromise = crypto.subtle.digest('SHA-256', encryptedData);
115
115
 
116
116
  return {
117
117
  type: thumbnail.type,
118
118
  encryptedData: encryptedData,
119
119
  originalSize: thumbnail.thumbnail.length,
120
120
  encryptedSize: encryptedData.length,
121
- hash: new Uint8Array(digest),
121
+ hashPromise: digestPromise.then((digest) => new Uint8Array<ArrayBuffer>(digest)),
122
122
  };
123
123
  }
124
124
 
125
125
  async encryptBlock(
126
- verifyBlock: (encryptedBlock: Uint8Array) => Promise<{ verificationToken: Uint8Array }>,
126
+ verifyBlock: (encryptedBlock: Uint8Array<ArrayBuffer>) => Promise<{ verificationToken: Uint8Array<ArrayBuffer> }>,
127
127
  nodeRevisionDraftKeys: NodeRevisionDraftKeys,
128
- block: Uint8Array,
128
+ block: Uint8Array<ArrayBuffer>,
129
129
  index: number,
130
130
  ): Promise<EncryptedBlock> {
131
131
  const { encryptedData, armoredSignature } = await this.driveCrypto.encryptBlock(
@@ -134,8 +134,7 @@ export class UploadCryptoService {
134
134
  nodeRevisionDraftKeys.contentKeyPacketSessionKey,
135
135
  nodeRevisionDraftKeys.signingKeys.contentSigningKey,
136
136
  );
137
-
138
- const digest = await crypto.subtle.digest('SHA-256', encryptedData);
137
+ const digestPromise = crypto.subtle.digest('SHA-256', encryptedData);
139
138
  const { verificationToken } = await verifyBlock(encryptedData);
140
139
 
141
140
  return {
@@ -145,13 +144,13 @@ export class UploadCryptoService {
145
144
  verificationToken,
146
145
  originalSize: block.length,
147
146
  encryptedSize: encryptedData.length,
148
- hash: new Uint8Array(digest),
147
+ hashPromise: digestPromise.then((digest) => new Uint8Array<ArrayBuffer>(digest)),
149
148
  };
150
149
  }
151
150
 
152
151
  async commitFile(
153
152
  nodeRevisionDraftKeys: NodeRevisionDraftKeys,
154
- manifest: Uint8Array,
153
+ manifest: Uint8Array<ArrayBuffer>,
155
154
  extendedAttributes?: string,
156
155
  ): Promise<{
157
156
  armoredManifestSignature: string;
@@ -190,10 +189,10 @@ export class UploadCryptoService {
190
189
 
191
190
  async verifyBlock(
192
191
  contentKeyPacketSessionKey: SessionKey,
193
- verificationCode: Uint8Array,
194
- encryptedData: Uint8Array,
192
+ verificationCode: Uint8Array<ArrayBuffer>,
193
+ encryptedData: Uint8Array<ArrayBuffer>,
195
194
  ): Promise<{
196
- verificationToken: Uint8Array;
195
+ verificationToken: Uint8Array<ArrayBuffer>;
197
196
  }> {
198
197
  // Attempt to decrypt data block, to try to detect bitflips / bad hardware
199
198
  //
@@ -8,7 +8,7 @@ export type NodeRevisionDraft = {
8
8
  nodeRevisionUid: string;
9
9
  nodeKeys: NodeRevisionDraftKeys;
10
10
  parentNodeKeys?: {
11
- hashKey: Uint8Array;
11
+ hashKey: Uint8Array<ArrayBuffer>;
12
12
  };
13
13
  // newNodeInfo is set only when revision is created with the new node.
14
14
  newNodeInfo?: {
@@ -64,19 +64,19 @@ export type NodeCryptoSigningKeys = {
64
64
  export type EncryptedBlockMetadata = {
65
65
  encryptedSize: number;
66
66
  originalSize: number;
67
- hash: Uint8Array;
67
+ hashPromise: Promise<Uint8Array<ArrayBuffer>>;
68
68
  };
69
69
 
70
70
  export type EncryptedBlock = EncryptedBlockMetadata & {
71
71
  index: number;
72
- encryptedData: Uint8Array;
72
+ encryptedData: Uint8Array<ArrayBuffer>;
73
73
  armoredSignature: string;
74
- verificationToken: Uint8Array;
74
+ verificationToken: Uint8Array<ArrayBuffer>;
75
75
  };
76
76
 
77
77
  export type EncryptedThumbnail = EncryptedBlockMetadata & {
78
78
  type: ThumbnailType;
79
- encryptedData: Uint8Array;
79
+ encryptedData: Uint8Array<ArrayBuffer>;
80
80
  };
81
81
 
82
82
  export type UploadTokens = {
@@ -101,7 +101,7 @@ export interface NodesService {
101
101
  key: PrivateKey;
102
102
  passphraseSessionKey: SessionKey;
103
103
  contentKeyPacketSessionKey?: SessionKey;
104
- hashKey?: Uint8Array;
104
+ hashKey?: Uint8Array<ArrayBuffer>;
105
105
  }>;
106
106
  getNodeSigningKeys(
107
107
  uids: { nodeUid: string; parentNodeUid?: string } | { nodeUid?: string; parentNodeUid: string },
@@ -73,7 +73,7 @@ export class UploadManager {
73
73
 
74
74
  private async createDraftOnAPI(
75
75
  parentFolderUid: string,
76
- parentHashKey: Uint8Array,
76
+ parentHashKey: Uint8Array<ArrayBuffer>,
77
77
  name: string,
78
78
  metadata: UploadMetadata,
79
79
  generatedNodeCrypto: NodeCrypto,
@@ -226,7 +226,7 @@ export class UploadManager {
226
226
 
227
227
  async commitDraft(
228
228
  nodeRevisionDraft: NodeRevisionDraft,
229
- manifest: Uint8Array,
229
+ manifest: Uint8Array<ArrayBuffer>,
230
230
  extendedAttributes: {
231
231
  modificationTime?: Date;
232
232
  size: number;