@protontech/drive-sdk 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/dist/crypto/driveCrypto.d.ts +1 -1
  2. package/dist/crypto/driveCrypto.js.map +1 -1
  3. package/dist/crypto/interface.d.ts +6 -1
  4. package/dist/crypto/openPGPCrypto.d.ts +1 -1
  5. package/dist/crypto/openPGPCrypto.js +4 -1
  6. package/dist/crypto/openPGPCrypto.js.map +1 -1
  7. package/dist/diagnostic/httpClient.d.ts +3 -3
  8. package/dist/interface/httpClient.d.ts +5 -5
  9. package/dist/interface/index.d.ts +15 -5
  10. package/dist/internal/apiService/apiService.js +1 -1
  11. package/dist/internal/apiService/apiService.js.map +1 -1
  12. package/dist/internal/apiService/errorCodes.d.ts +1 -0
  13. package/dist/internal/apiService/errorCodes.js.map +1 -1
  14. package/dist/internal/apiService/errors.d.ts +4 -3
  15. package/dist/internal/apiService/errors.js +7 -4
  16. package/dist/internal/apiService/errors.js.map +1 -1
  17. package/dist/internal/apiService/errors.test.js +2 -1
  18. package/dist/internal/apiService/errors.test.js.map +1 -1
  19. package/dist/internal/download/cryptoService.js +2 -2
  20. package/dist/internal/download/cryptoService.js.map +1 -1
  21. package/dist/internal/download/fileDownloader.js +2 -2
  22. package/dist/internal/download/fileDownloader.js.map +1 -1
  23. package/dist/internal/download/fileDownloader.test.js +3 -1
  24. package/dist/internal/download/fileDownloader.test.js.map +1 -1
  25. package/dist/internal/events/index.d.ts +1 -1
  26. package/dist/internal/nodes/cache.js +3 -1
  27. package/dist/internal/nodes/cache.js.map +1 -1
  28. package/dist/internal/nodes/cryptoCache.js +6 -7
  29. package/dist/internal/nodes/cryptoCache.js.map +1 -1
  30. package/dist/internal/nodes/cryptoCache.test.js +4 -7
  31. package/dist/internal/nodes/cryptoCache.test.js.map +1 -1
  32. package/dist/internal/nodes/cryptoReporter.d.ts +20 -0
  33. package/dist/internal/nodes/cryptoReporter.js +96 -0
  34. package/dist/internal/nodes/cryptoReporter.js.map +1 -0
  35. package/dist/internal/nodes/cryptoService.d.ts +17 -12
  36. package/dist/internal/nodes/cryptoService.js +17 -97
  37. package/dist/internal/nodes/cryptoService.js.map +1 -1
  38. package/dist/internal/nodes/cryptoService.test.js +3 -1
  39. package/dist/internal/nodes/cryptoService.test.js.map +1 -1
  40. package/dist/internal/nodes/index.js +3 -1
  41. package/dist/internal/nodes/index.js.map +1 -1
  42. package/dist/internal/nodes/interface.d.ts +1 -1
  43. package/dist/internal/nodes/nodesAccess.d.ts +2 -2
  44. package/dist/internal/nodes/nodesAccess.js +52 -54
  45. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  46. package/dist/internal/shares/cryptoCache.d.ts +4 -3
  47. package/dist/internal/shares/cryptoCache.js +23 -6
  48. package/dist/internal/shares/cryptoCache.js.map +1 -1
  49. package/dist/internal/shares/cryptoCache.test.js +3 -2
  50. package/dist/internal/shares/cryptoCache.test.js.map +1 -1
  51. package/dist/internal/shares/index.js +1 -1
  52. package/dist/internal/shares/index.js.map +1 -1
  53. package/dist/internal/sharing/cache.d.ts +3 -0
  54. package/dist/internal/sharing/cache.js +17 -2
  55. package/dist/internal/sharing/cache.js.map +1 -1
  56. package/dist/internal/sharing/cryptoService.js +8 -6
  57. package/dist/internal/sharing/cryptoService.js.map +1 -1
  58. package/dist/internal/sharing/cryptoService.test.js +13 -0
  59. package/dist/internal/sharing/cryptoService.test.js.map +1 -1
  60. package/dist/internal/sharing/index.js +1 -1
  61. package/dist/internal/sharing/index.js.map +1 -1
  62. package/dist/internal/sharing/interface.d.ts +1 -1
  63. package/dist/internal/sharing/interface.js +1 -1
  64. package/dist/internal/sharing/sharingAccess.js +6 -0
  65. package/dist/internal/sharing/sharingAccess.js.map +1 -1
  66. package/dist/internal/sharing/sharingAccess.test.js +242 -33
  67. package/dist/internal/sharing/sharingAccess.test.js.map +1 -1
  68. package/dist/internal/sharing/sharingManagement.d.ts +3 -1
  69. package/dist/internal/sharing/sharingManagement.js +10 -1
  70. package/dist/internal/sharing/sharingManagement.js.map +1 -1
  71. package/dist/internal/sharing/sharingManagement.test.js +32 -1
  72. package/dist/internal/sharing/sharingManagement.test.js.map +1 -1
  73. package/dist/internal/sharingPublic/apiService.d.ts +19 -0
  74. package/dist/internal/sharingPublic/apiService.js +141 -0
  75. package/dist/internal/sharingPublic/apiService.js.map +1 -0
  76. package/dist/internal/sharingPublic/cryptoCache.d.ts +19 -0
  77. package/dist/internal/sharingPublic/cryptoCache.js +72 -0
  78. package/dist/internal/sharingPublic/cryptoCache.js.map +1 -0
  79. package/dist/internal/sharingPublic/cryptoService.d.ts +9 -0
  80. package/dist/internal/sharingPublic/cryptoService.js +57 -0
  81. package/dist/internal/sharingPublic/cryptoService.js.map +1 -0
  82. package/dist/internal/sharingPublic/index.d.ts +15 -0
  83. package/dist/internal/sharingPublic/index.js +27 -0
  84. package/dist/internal/sharingPublic/index.js.map +1 -0
  85. package/dist/internal/sharingPublic/interface.d.ts +6 -0
  86. package/dist/internal/sharingPublic/interface.js +3 -0
  87. package/dist/internal/sharingPublic/interface.js.map +1 -0
  88. package/dist/internal/sharingPublic/manager.d.ts +19 -0
  89. package/dist/internal/sharingPublic/manager.js +81 -0
  90. package/dist/internal/sharingPublic/manager.js.map +1 -0
  91. package/dist/internal/sharingPublic/session/apiService.d.ts +28 -0
  92. package/dist/internal/sharingPublic/session/apiService.js +55 -0
  93. package/dist/internal/sharingPublic/session/apiService.js.map +1 -0
  94. package/dist/internal/sharingPublic/session/httpClient.d.ts +16 -0
  95. package/dist/internal/sharingPublic/session/httpClient.js +41 -0
  96. package/dist/internal/sharingPublic/session/httpClient.js.map +1 -0
  97. package/dist/internal/sharingPublic/session/index.d.ts +1 -0
  98. package/dist/internal/sharingPublic/session/index.js +6 -0
  99. package/dist/internal/sharingPublic/session/index.js.map +1 -0
  100. package/dist/internal/sharingPublic/session/interface.d.ts +18 -0
  101. package/dist/internal/sharingPublic/session/interface.js +3 -0
  102. package/dist/internal/sharingPublic/session/interface.js.map +1 -0
  103. package/dist/internal/sharingPublic/session/manager.d.ts +49 -0
  104. package/dist/internal/sharingPublic/session/manager.js +75 -0
  105. package/dist/internal/sharingPublic/session/manager.js.map +1 -0
  106. package/dist/internal/sharingPublic/session/session.d.ts +34 -0
  107. package/dist/internal/sharingPublic/session/session.js +67 -0
  108. package/dist/internal/sharingPublic/session/session.js.map +1 -0
  109. package/dist/internal/sharingPublic/session/url.d.ts +12 -0
  110. package/dist/internal/sharingPublic/session/url.js +23 -0
  111. package/dist/internal/sharingPublic/session/url.js.map +1 -0
  112. package/dist/internal/sharingPublic/session/url.test.d.ts +1 -0
  113. package/dist/internal/sharingPublic/session/url.test.js +59 -0
  114. package/dist/internal/sharingPublic/session/url.test.js.map +1 -0
  115. package/dist/internal/upload/streamUploader.js +1 -1
  116. package/dist/internal/upload/streamUploader.js.map +1 -1
  117. package/dist/internal/upload/streamUploader.test.js +3 -1
  118. package/dist/internal/upload/streamUploader.test.js.map +1 -1
  119. package/dist/protonDriveClient.d.ts +18 -3
  120. package/dist/protonDriveClient.js +31 -8
  121. package/dist/protonDriveClient.js.map +1 -1
  122. package/dist/protonDrivePublicLinkClient.d.ts +57 -0
  123. package/dist/protonDrivePublicLinkClient.js +73 -0
  124. package/dist/protonDrivePublicLinkClient.js.map +1 -0
  125. package/package.json +1 -1
  126. package/src/crypto/driveCrypto.ts +1 -1
  127. package/src/crypto/interface.ts +12 -1
  128. package/src/crypto/openPGPCrypto.ts +5 -2
  129. package/src/diagnostic/httpClient.ts +4 -4
  130. package/src/interface/httpClient.ts +5 -5
  131. package/src/interface/index.ts +18 -6
  132. package/src/internal/apiService/apiService.ts +1 -1
  133. package/src/internal/apiService/errorCodes.ts +1 -0
  134. package/src/internal/apiService/errors.test.ts +2 -1
  135. package/src/internal/apiService/errors.ts +15 -4
  136. package/src/internal/download/cryptoService.ts +2 -2
  137. package/src/internal/download/fileDownloader.test.ts +3 -1
  138. package/src/internal/download/fileDownloader.ts +2 -2
  139. package/src/internal/events/index.ts +1 -1
  140. package/src/internal/nodes/cache.ts +3 -1
  141. package/src/internal/nodes/cryptoCache.test.ts +4 -7
  142. package/src/internal/nodes/cryptoCache.ts +6 -7
  143. package/src/internal/nodes/cryptoReporter.ts +145 -0
  144. package/src/internal/nodes/cryptoService.test.ts +3 -1
  145. package/src/internal/nodes/cryptoService.ts +44 -137
  146. package/src/internal/nodes/index.ts +3 -1
  147. package/src/internal/nodes/interface.ts +3 -1
  148. package/src/internal/nodes/nodesAccess.ts +59 -61
  149. package/src/internal/shares/cryptoCache.test.ts +3 -2
  150. package/src/internal/shares/cryptoCache.ts +26 -7
  151. package/src/internal/shares/index.ts +1 -1
  152. package/src/internal/sharing/cache.ts +19 -2
  153. package/src/internal/sharing/cryptoService.test.ts +22 -1
  154. package/src/internal/sharing/cryptoService.ts +8 -6
  155. package/src/internal/sharing/index.ts +1 -0
  156. package/src/internal/sharing/interface.ts +1 -1
  157. package/src/internal/sharing/sharingAccess.test.ts +282 -34
  158. package/src/internal/sharing/sharingAccess.ts +6 -0
  159. package/src/internal/sharing/sharingManagement.test.ts +33 -0
  160. package/src/internal/sharing/sharingManagement.ts +9 -0
  161. package/src/internal/sharingPublic/apiService.ts +173 -0
  162. package/src/internal/sharingPublic/cryptoCache.ts +79 -0
  163. package/src/internal/sharingPublic/cryptoService.ts +98 -0
  164. package/src/internal/sharingPublic/index.ts +41 -0
  165. package/src/internal/sharingPublic/interface.ts +14 -0
  166. package/src/internal/sharingPublic/manager.ts +86 -0
  167. package/src/internal/sharingPublic/session/apiService.ts +74 -0
  168. package/src/internal/sharingPublic/session/httpClient.ts +48 -0
  169. package/src/internal/sharingPublic/session/index.ts +1 -0
  170. package/src/internal/sharingPublic/session/interface.ts +20 -0
  171. package/src/internal/sharingPublic/session/manager.ts +97 -0
  172. package/src/internal/sharingPublic/session/session.ts +78 -0
  173. package/src/internal/sharingPublic/session/url.test.ts +72 -0
  174. package/src/internal/sharingPublic/session/url.ts +23 -0
  175. package/src/internal/upload/streamUploader.test.ts +3 -1
  176. package/src/internal/upload/streamUploader.ts +1 -1
  177. package/src/protonDriveClient.ts +48 -11
  178. package/src/protonDrivePublicLinkClient.ts +135 -0
@@ -10,12 +10,14 @@ import {
10
10
  resultOk,
11
11
  } from '../../interface';
12
12
  import { SharingAPIService } from './apiService';
13
+ import { SharingCache } from './cache';
13
14
  import { SharingCryptoService } from './cryptoService';
14
15
  import { SharesService, NodesService } from './interface';
15
16
  import { SharingManagement } from './sharingManagement';
16
17
 
17
18
  describe('SharingManagement', () => {
18
19
  let apiService: SharingAPIService;
20
+ let cache: SharingCache;
19
21
  let cryptoService: SharingCryptoService;
20
22
  let accountService: ProtonDriveAccount;
21
23
  let sharesService: SharesService;
@@ -57,6 +59,12 @@ describe('SharingManagement', () => {
57
59
  updatePublicLink: jest.fn(),
58
60
  };
59
61
  // @ts-expect-error No need to implement all methods for mocking
62
+ cache = {
63
+ hasSharedByMeNodeUidsLoaded: jest.fn().mockResolvedValue(true),
64
+ addSharedByMeNodeUid: jest.fn(),
65
+ removeSharedByMeNodeUid: jest.fn(),
66
+ };
67
+ // @ts-expect-error No need to implement all methods for mocking
60
68
  cryptoService = {
61
69
  generateShareKeys: jest.fn().mockResolvedValue({
62
70
  shareKey: { encrypted: 'encrypted-key', decrypted: { passphraseSessionKey: 'pass-session-key' } },
@@ -104,6 +112,7 @@ describe('SharingManagement', () => {
104
112
  sharingManagement = new SharingManagement(
105
113
  getMockLogger(),
106
114
  apiService,
115
+ cache,
107
116
  cryptoService,
108
117
  accountService,
109
118
  sharesService,
@@ -214,6 +223,7 @@ describe('SharingManagement', () => {
214
223
  expect(apiService.updateInvitation).not.toHaveBeenCalled();
215
224
  expect(apiService.inviteProtonUser).toHaveBeenCalled();
216
225
  expect(nodesService.notifyNodeChanged).toHaveBeenCalledWith(nodeUid);
226
+ expect(cache.addSharedByMeNodeUid).toHaveBeenCalledWith(nodeUid);
217
227
  });
218
228
  });
219
229
 
@@ -279,6 +289,7 @@ describe('SharingManagement', () => {
279
289
  });
280
290
  expect(apiService.updateInvitation).not.toHaveBeenCalled();
281
291
  expect(apiService.inviteProtonUser).toHaveBeenCalled();
292
+ expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
282
293
  });
283
294
 
284
295
  it('should share node with proton email with specific role', async () => {
@@ -302,6 +313,7 @@ describe('SharingManagement', () => {
302
313
  });
303
314
  expect(apiService.updateInvitation).not.toHaveBeenCalled();
304
315
  expect(apiService.inviteProtonUser).toHaveBeenCalled();
316
+ expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
305
317
  });
306
318
 
307
319
  it('should update existing role', async () => {
@@ -322,6 +334,7 @@ describe('SharingManagement', () => {
322
334
  });
323
335
  expect(apiService.updateInvitation).toHaveBeenCalled();
324
336
  expect(apiService.inviteProtonUser).not.toHaveBeenCalled();
337
+ expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
325
338
  });
326
339
 
327
340
  it('should be no-op if no change', async () => {
@@ -337,6 +350,7 @@ describe('SharingManagement', () => {
337
350
  });
338
351
  expect(apiService.updateInvitation).not.toHaveBeenCalled();
339
352
  expect(apiService.inviteProtonUser).not.toHaveBeenCalled();
353
+ expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
340
354
  });
341
355
 
342
356
  it('should use address from the root node context share', async () => {
@@ -383,6 +397,7 @@ describe('SharingManagement', () => {
383
397
  });
384
398
  expect(apiService.updateExternalInvitation).not.toHaveBeenCalled();
385
399
  expect(apiService.inviteExternalUser).toHaveBeenCalled();
400
+ expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
386
401
  });
387
402
 
388
403
  it('should share node with external email with specific role', async () => {
@@ -407,6 +422,7 @@ describe('SharingManagement', () => {
407
422
  });
408
423
  expect(apiService.updateExternalInvitation).not.toHaveBeenCalled();
409
424
  expect(apiService.inviteExternalUser).toHaveBeenCalled();
425
+ expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
410
426
  });
411
427
 
412
428
  it('should update existing role', async () => {
@@ -427,6 +443,7 @@ describe('SharingManagement', () => {
427
443
  });
428
444
  expect(apiService.updateExternalInvitation).toHaveBeenCalled();
429
445
  expect(apiService.inviteExternalUser).not.toHaveBeenCalled();
446
+ expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
430
447
  });
431
448
 
432
449
  it('should be no-op if no change', async () => {
@@ -442,6 +459,7 @@ describe('SharingManagement', () => {
442
459
  });
443
460
  expect(apiService.updateExternalInvitation).not.toHaveBeenCalled();
444
461
  expect(apiService.inviteExternalUser).not.toHaveBeenCalled();
462
+ expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
445
463
  });
446
464
 
447
465
  it('should use address from the root node context share', async () => {
@@ -512,6 +530,7 @@ describe('SharingManagement', () => {
512
530
  }),
513
531
  expect.anything(),
514
532
  );
533
+ expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
515
534
  });
516
535
  });
517
536
 
@@ -535,6 +554,7 @@ describe('SharingManagement', () => {
535
554
  expect(apiService.updateMember).toHaveBeenCalled();
536
555
  expect(apiService.updateInvitation).not.toHaveBeenCalled();
537
556
  expect(apiService.inviteProtonUser).not.toHaveBeenCalled();
557
+ expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
538
558
  });
539
559
 
540
560
  it('should be no-op if no change via proton user', async () => {
@@ -551,6 +571,7 @@ describe('SharingManagement', () => {
551
571
  expect(apiService.updateMember).not.toHaveBeenCalled();
552
572
  expect(apiService.updateInvitation).not.toHaveBeenCalled();
553
573
  expect(apiService.inviteProtonUser).not.toHaveBeenCalled();
574
+ expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
554
575
  });
555
576
 
556
577
  it('should update member via non-proton user', async () => {
@@ -572,6 +593,7 @@ describe('SharingManagement', () => {
572
593
  expect(apiService.updateMember).toHaveBeenCalled();
573
594
  expect(apiService.updateInvitation).not.toHaveBeenCalled();
574
595
  expect(apiService.inviteProtonUser).not.toHaveBeenCalled();
596
+ expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
575
597
  });
576
598
 
577
599
  it('should be no-op if no change via non-proton user', async () => {
@@ -588,6 +610,7 @@ describe('SharingManagement', () => {
588
610
  expect(apiService.updateMember).not.toHaveBeenCalled();
589
611
  expect(apiService.updateInvitation).not.toHaveBeenCalled();
590
612
  expect(apiService.inviteProtonUser).not.toHaveBeenCalled();
613
+ expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
591
614
  });
592
615
  });
593
616
 
@@ -635,6 +658,7 @@ describe('SharingManagement', () => {
635
658
  srp: 'publicLinkSrp',
636
659
  }),
637
660
  );
661
+ expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
638
662
  });
639
663
 
640
664
  it('should share node with custom password and expiration', async () => {
@@ -680,6 +704,7 @@ describe('SharingManagement', () => {
680
704
  srp: 'publicLinkSrp',
681
705
  }),
682
706
  );
707
+ expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
683
708
  });
684
709
 
685
710
  it('should update public link with custom password and expiration', async () => {
@@ -734,6 +759,7 @@ describe('SharingManagement', () => {
734
759
  srp: 'publicLinkSrp',
735
760
  }),
736
761
  );
762
+ expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
737
763
  });
738
764
 
739
765
  it('should not allow updating legacy public link', async () => {
@@ -839,6 +865,7 @@ describe('SharingManagement', () => {
839
865
  expect(apiService.deleteExternalInvitation).not.toHaveBeenCalled();
840
866
  expect(apiService.removeMember).not.toHaveBeenCalled();
841
867
  expect(apiService.removePublicLink).not.toHaveBeenCalled();
868
+ expect(cache.removeSharedByMeNodeUid).not.toHaveBeenCalled();
842
869
  });
843
870
 
844
871
  it('should delete external invitation', async () => {
@@ -855,6 +882,7 @@ describe('SharingManagement', () => {
855
882
  expect(apiService.deleteExternalInvitation).toHaveBeenCalled();
856
883
  expect(apiService.removeMember).not.toHaveBeenCalled();
857
884
  expect(apiService.removePublicLink).not.toHaveBeenCalled();
885
+ expect(cache.removeSharedByMeNodeUid).not.toHaveBeenCalled();
858
886
  });
859
887
 
860
888
  it('should remove member', async () => {
@@ -871,6 +899,7 @@ describe('SharingManagement', () => {
871
899
  expect(apiService.deleteExternalInvitation).not.toHaveBeenCalled();
872
900
  expect(apiService.removeMember).toHaveBeenCalled();
873
901
  expect(apiService.removePublicLink).not.toHaveBeenCalled();
902
+ expect(cache.removeSharedByMeNodeUid).not.toHaveBeenCalled();
874
903
  });
875
904
 
876
905
  it('should be no-op if not shared with email', async () => {
@@ -887,6 +916,7 @@ describe('SharingManagement', () => {
887
916
  expect(apiService.deleteExternalInvitation).not.toHaveBeenCalled();
888
917
  expect(apiService.removeMember).not.toHaveBeenCalled();
889
918
  expect(apiService.removePublicLink).not.toHaveBeenCalled();
919
+ expect(cache.removeSharedByMeNodeUid).not.toHaveBeenCalled();
890
920
  });
891
921
 
892
922
  it('should remove public link', async () => {
@@ -903,6 +933,7 @@ describe('SharingManagement', () => {
903
933
  expect(apiService.deleteExternalInvitation).not.toHaveBeenCalled();
904
934
  expect(apiService.removeMember).not.toHaveBeenCalled();
905
935
  expect(apiService.removePublicLink).toHaveBeenCalled();
936
+ expect(cache.removeSharedByMeNodeUid).not.toHaveBeenCalled();
906
937
  });
907
938
 
908
939
  it('should remove share if all is removed', async () => {
@@ -915,6 +946,7 @@ describe('SharingManagement', () => {
915
946
  expect(apiService.removeMember).not.toHaveBeenCalled();
916
947
  expect(apiService.removePublicLink).not.toHaveBeenCalled();
917
948
  expect(nodesService.notifyNodeChanged).toHaveBeenCalled();
949
+ expect(cache.removeSharedByMeNodeUid).toHaveBeenCalledWith(nodeUid);
918
950
  });
919
951
 
920
952
  it('should remove share if everything is manually removed', async () => {
@@ -929,6 +961,7 @@ describe('SharingManagement', () => {
929
961
  expect(apiService.deleteExternalInvitation).toHaveBeenCalled();
930
962
  expect(apiService.removeMember).toHaveBeenCalled();
931
963
  expect(apiService.removePublicLink).toHaveBeenCalled();
964
+ expect(cache.removeSharedByMeNodeUid).toHaveBeenCalledWith(nodeUid);
932
965
  });
933
966
  });
934
967
 
@@ -20,6 +20,7 @@ import { getErrorMessage } from '../errors';
20
20
  import { SharingAPIService } from './apiService';
21
21
  import { PUBLIC_LINK_GENERATED_PASSWORD_LENGTH, SharingCryptoService } from './cryptoService';
22
22
  import { SharesService, NodesService, ShareResultWithCreatorEmail, PublicLinkWithCreatorEmail } from './interface';
23
+ import { SharingCache } from './cache';
23
24
 
24
25
  interface InternalShareResult extends ShareResultWithCreatorEmail {
25
26
  share: Share;
@@ -54,6 +55,7 @@ export class SharingManagement {
54
55
  constructor(
55
56
  private logger: Logger,
56
57
  private apiService: SharingAPIService,
58
+ private cache: SharingCache,
57
59
  private cryptoService: SharingCryptoService,
58
60
  private account: ProtonDriveAccount,
59
61
  private sharesService: SharesService,
@@ -61,6 +63,7 @@ export class SharingManagement {
61
63
  ) {
62
64
  this.logger = logger;
63
65
  this.apiService = apiService;
66
+ this.cache = cache;
64
67
  this.cryptoService = cryptoService;
65
68
  this.account = account;
66
69
  this.sharesService = sharesService;
@@ -409,6 +412,9 @@ export class SharingManagement {
409
412
  base64NameKeyPacket: keys.base64NameKeyPacket,
410
413
  });
411
414
  await this.nodesService.notifyNodeChanged(nodeUid);
415
+ if (await this.cache.hasSharedByMeNodeUidsLoaded()) {
416
+ await this.cache.addSharedByMeNodeUid(nodeUid);
417
+ }
412
418
 
413
419
  const share = {
414
420
  volumeId,
@@ -430,6 +436,9 @@ export class SharingManagement {
430
436
  private async deleteShare(shareId: string, nodeUid: string): Promise<void> {
431
437
  await this.apiService.deleteShare(shareId);
432
438
  await this.nodesService.notifyNodeChanged(nodeUid);
439
+ if (await this.cache.hasSharedByMeNodeUidsLoaded()) {
440
+ await this.cache.removeSharedByMeNodeUid(nodeUid);
441
+ }
433
442
  }
434
443
 
435
444
  private async inviteProtonUser(
@@ -0,0 +1,173 @@
1
+ import { DriveAPIService, drivePaths, nodeTypeNumberToNodeType } from '../apiService';
2
+ import { Logger, MemberRole } from '../../interface';
3
+ import { makeNodeUid, splitNodeUid } from '../uids';
4
+ import { EncryptedShareCrypto, EncryptedNode } from './interface';
5
+
6
+ const PAGE_SIZE = 50;
7
+
8
+ type GetTokenInfoResponse = drivePaths['/drive/urls/{token}']['get']['responses']['200']['content']['application/json'];
9
+
10
+ type GetTokenFolderChildrenResponse =
11
+ drivePaths['/drive/urls/{token}/folders/{linkID}/children']['get']['responses']['200']['content']['application/json'];
12
+
13
+ /**
14
+ * Provides API communication for accessing public link data.
15
+ *
16
+ * The service is responsible for transforming local objects to API payloads
17
+ * and vice versa. It should not contain any business logic.
18
+ */
19
+ export class SharingPublicAPIService {
20
+ constructor(
21
+ private logger: Logger,
22
+ private apiService: DriveAPIService,
23
+ ) {
24
+ this.logger = logger;
25
+ this.apiService = apiService;
26
+ }
27
+
28
+ async getPublicLinkRoot(token: string): Promise<{
29
+ encryptedNode: EncryptedNode;
30
+ encryptedShare: EncryptedShareCrypto;
31
+ }> {
32
+ const response = await this.apiService.get<GetTokenInfoResponse>(`drive/urls/${token}`);
33
+ const encryptedNode = tokenToEncryptedNode(this.logger, response.Token);
34
+
35
+ return {
36
+ encryptedNode: encryptedNode,
37
+ encryptedShare: {
38
+ base64UrlPasswordSalt: response.Token.SharePasswordSalt,
39
+ armoredKey: response.Token.ShareKey,
40
+ armoredPassphrase: response.Token.SharePassphrase,
41
+ },
42
+ };
43
+ }
44
+
45
+ async *iterateFolderChildren(parentUid: string, signal?: AbortSignal): AsyncGenerator<EncryptedNode> {
46
+ const { volumeId: token, nodeId } = splitNodeUid(parentUid);
47
+
48
+ let page = 0;
49
+ while (true) {
50
+ const response = await this.apiService.get<GetTokenFolderChildrenResponse>(
51
+ `drive/urls/${token}/folders/${nodeId}/children?Page=${page}&PageSize=${PAGE_SIZE}`,
52
+ signal,
53
+ );
54
+
55
+ for (const link of response.Links) {
56
+ yield linkToEncryptedNode(this.logger, token, link);
57
+ }
58
+
59
+ if (response.Links.length < PAGE_SIZE) {
60
+ break;
61
+ }
62
+ page++;
63
+ }
64
+ }
65
+ }
66
+
67
+ function tokenToEncryptedNode(logger: Logger, token: GetTokenInfoResponse['Token']): EncryptedNode {
68
+ const baseNodeMetadata = {
69
+ // Internal metadata
70
+ encryptedName: token.Name,
71
+
72
+ // Basic node metadata
73
+ uid: makeNodeUid(token.Token, token.LinkID),
74
+ parentUid: undefined,
75
+ type: nodeTypeNumberToNodeType(logger, token.LinkType),
76
+ creationTime: new Date(), // TODO
77
+
78
+ isShared: false,
79
+ directRole: MemberRole.Viewer, // TODO
80
+ };
81
+
82
+ const baseCryptoNodeMetadata = {
83
+ signatureEmail: token.SignatureEmail || undefined,
84
+ armoredKey: token.NodeKey,
85
+ armoredNodePassphrase: token.NodePassphrase,
86
+ armoredNodePassphraseSignature: token.NodePassphraseSignature || undefined,
87
+ };
88
+
89
+ if (token.LinkType === 1 && token.NodeHashKey) {
90
+ return {
91
+ ...baseNodeMetadata,
92
+ encryptedCrypto: {
93
+ ...baseCryptoNodeMetadata,
94
+ folder: {
95
+ armoredHashKey: token.NodeHashKey as string,
96
+ },
97
+ },
98
+ };
99
+ }
100
+
101
+ if (token.LinkType === 2 && token.ContentKeyPacket) {
102
+ return {
103
+ ...baseNodeMetadata,
104
+ totalStorageSize: token.Size || undefined,
105
+ mediaType: token.MIMEType || undefined,
106
+ encryptedCrypto: {
107
+ ...baseCryptoNodeMetadata,
108
+ file: {
109
+ base64ContentKeyPacket: token.ContentKeyPacket,
110
+ },
111
+ },
112
+ };
113
+ }
114
+
115
+ throw new Error(`Unknown node type: ${token.LinkType}`);
116
+ }
117
+
118
+ function linkToEncryptedNode(
119
+ logger: Logger,
120
+ token: string,
121
+ link: GetTokenFolderChildrenResponse['Links'][0],
122
+ ): EncryptedNode {
123
+ const baseNodeMetadata = {
124
+ // Internal metadata
125
+ hash: link.Hash || undefined,
126
+ encryptedName: link.Name,
127
+
128
+ // Basic node metadata
129
+ uid: makeNodeUid(token, link.LinkID),
130
+ parentUid: link.ParentLinkID ? makeNodeUid(token, link.ParentLinkID) : undefined,
131
+ type: nodeTypeNumberToNodeType(logger, link.Type),
132
+ creationTime: new Date(), // TODO
133
+ totalStorageSize: link.TotalSize,
134
+
135
+ isShared: false,
136
+ directRole: MemberRole.Viewer, // TODO
137
+ };
138
+
139
+ const baseCryptoNodeMetadata = {
140
+ signatureEmail: link.SignatureEmail || undefined,
141
+ armoredKey: link.NodeKey,
142
+ armoredNodePassphrase: link.NodePassphrase,
143
+ armoredNodePassphraseSignature: link.NodePassphraseSignature || undefined,
144
+ };
145
+
146
+ if (link.Type === 1 && link.FolderProperties) {
147
+ return {
148
+ ...baseNodeMetadata,
149
+ encryptedCrypto: {
150
+ ...baseCryptoNodeMetadata,
151
+ folder: {
152
+ armoredHashKey: link.FolderProperties.NodeHashKey as string,
153
+ },
154
+ },
155
+ };
156
+ }
157
+
158
+ if (link.Type === 2 && link.FileProperties?.ContentKeyPacket) {
159
+ return {
160
+ ...baseNodeMetadata,
161
+ totalStorageSize: link.FileProperties.ActiveRevision?.Size || undefined,
162
+ mediaType: link.MIMEType || undefined,
163
+ encryptedCrypto: {
164
+ ...baseCryptoNodeMetadata,
165
+ file: {
166
+ base64ContentKeyPacket: link.FileProperties.ContentKeyPacket,
167
+ },
168
+ },
169
+ };
170
+ }
171
+
172
+ throw new Error(`Unknown node type: ${link.Type}`);
173
+ }
@@ -0,0 +1,79 @@
1
+ import { PrivateKey } from '../../crypto';
2
+ import { ProtonDriveCryptoCache, Logger } from '../../interface';
3
+ import { DecryptedNodeKeys } from './interface';
4
+
5
+ /**
6
+ * Provides caching for public link crypto material.
7
+ *
8
+ * The cache is responsible for serialising and deserialising public link
9
+ * crypto material.
10
+ */
11
+ export class SharingPublicCryptoCache {
12
+ constructor(
13
+ private logger: Logger,
14
+ private driveCache: ProtonDriveCryptoCache,
15
+ ) {
16
+ this.logger = logger;
17
+ this.driveCache = driveCache;
18
+ }
19
+
20
+ async setShareKey(shareKey: PrivateKey): Promise<void> {
21
+ await this.driveCache.setEntity(getShareKeyCacheKey(), {
22
+ publicShareKey: {
23
+ key: shareKey,
24
+ },
25
+ });
26
+ }
27
+
28
+ async getShareKey(): Promise<PrivateKey> {
29
+ const shareKeyData = await this.driveCache.getEntity(getShareKeyCacheKey());
30
+ if (!shareKeyData.publicShareKey) {
31
+ try {
32
+ await this.driveCache.removeEntities([getShareKeyCacheKey()]);
33
+ } catch (removingError: unknown) {
34
+ this.logger.warn(
35
+ `Failed to remove corrupted public share key from the cache: ${removingError instanceof Error ? removingError.message : removingError}`,
36
+ );
37
+ }
38
+ throw new Error('Failed to deserialize public share key');
39
+ }
40
+ return shareKeyData.publicShareKey.key;
41
+ }
42
+
43
+ async setNodeKeys(nodeUid: string, keys: DecryptedNodeKeys): Promise<void> {
44
+ const cacheUid = getNodeCacheKey(nodeUid);
45
+ await this.driveCache.setEntity(cacheUid, {
46
+ nodeKeys: keys,
47
+ });
48
+ }
49
+
50
+ async getNodeKeys(nodeUid: string): Promise<DecryptedNodeKeys> {
51
+ const nodeKeysData = await this.driveCache.getEntity(getNodeCacheKey(nodeUid));
52
+ if (!nodeKeysData.nodeKeys) {
53
+ try {
54
+ await this.removeNodeKeys([nodeUid]);
55
+ } catch (removingError: unknown) {
56
+ // The node keys will not be returned, thus SDK will re-fetch
57
+ // and re-cache it. Setting it again should then fix the problem.
58
+ this.logger.warn(
59
+ `Failed to remove corrupted public node keys from the cache: ${removingError instanceof Error ? removingError.message : removingError}`,
60
+ );
61
+ }
62
+ throw new Error(`Failed to deserialize public node keys`);
63
+ }
64
+ return nodeKeysData.nodeKeys;
65
+ }
66
+
67
+ async removeNodeKeys(nodeUids: string[]): Promise<void> {
68
+ const cacheUids = nodeUids.map(getNodeCacheKey);
69
+ await this.driveCache.removeEntities(cacheUids);
70
+ }
71
+ }
72
+
73
+ function getShareKeyCacheKey() {
74
+ return 'publicShareKey';
75
+ }
76
+
77
+ function getNodeCacheKey(nodeUid: string) {
78
+ return `publicNodeKeys-${nodeUid}`;
79
+ }
@@ -0,0 +1,98 @@
1
+ import { c } from 'ttag';
2
+
3
+ import { DriveCrypto, PrivateKey, VERIFICATION_STATUS } from '../../crypto';
4
+ import { getVerificationMessage } from '../errors';
5
+ import {
6
+ resultOk,
7
+ resultError,
8
+ Author,
9
+ AnonymousUser,
10
+ ProtonDriveTelemetry,
11
+ MetricVerificationErrorField,
12
+ MetricVolumeType,
13
+ MetricsDecryptionErrorField,
14
+ Logger,
15
+ ProtonDriveAccount,
16
+ } from '../../interface';
17
+ import { NodesCryptoService } from '../nodes/cryptoService';
18
+ import { EncryptedShareCrypto } from './interface';
19
+
20
+ export class SharingPublicCryptoService extends NodesCryptoService {
21
+ constructor(
22
+ telemetry: ProtonDriveTelemetry,
23
+ driveCrypto: DriveCrypto,
24
+ account: ProtonDriveAccount,
25
+ private password: string,
26
+ ) {
27
+ super(telemetry, driveCrypto, account, new SharingPublicCryptoReporter(telemetry));
28
+ this.password = password;
29
+ }
30
+
31
+ async decryptPublicLinkShareKey(encryptedShare: EncryptedShareCrypto): Promise<PrivateKey> {
32
+ const { key: shareKey } = await this.driveCrypto.decryptKeyWithSrpPassword(
33
+ this.password,
34
+ encryptedShare.base64UrlPasswordSalt,
35
+ encryptedShare.armoredKey,
36
+ encryptedShare.armoredPassphrase,
37
+ );
38
+ return shareKey;
39
+ }
40
+ }
41
+
42
+ class SharingPublicCryptoReporter {
43
+ private logger: Logger;
44
+ private telemetry: ProtonDriveTelemetry;
45
+
46
+ constructor(telemetry: ProtonDriveTelemetry) {
47
+ this.telemetry = telemetry;
48
+ this.logger = telemetry.getLogger('sharingPublic-crypto');
49
+ }
50
+
51
+ async handleClaimedAuthor(
52
+ node: { uid: string; creationTime: Date },
53
+ field: MetricVerificationErrorField,
54
+ signatureType: string,
55
+ verified: VERIFICATION_STATUS,
56
+ verificationErrors?: Error[],
57
+ claimedAuthor?: string,
58
+ notAvailableVerificationKeys = false,
59
+ ): Promise<Author> {
60
+ if (verified === VERIFICATION_STATUS.SIGNED_AND_VALID) {
61
+ return resultOk(claimedAuthor || (null as AnonymousUser));
62
+ }
63
+
64
+ return resultError({
65
+ claimedAuthor,
66
+ error: !claimedAuthor
67
+ ? c('Info').t`Author is not provided on public link`
68
+ : getVerificationMessage(verified, verificationErrors, signatureType, notAvailableVerificationKeys),
69
+ });
70
+ }
71
+
72
+ reportDecryptionError(
73
+ node: { uid: string; creationTime: Date },
74
+ field: MetricsDecryptionErrorField,
75
+ error: unknown,
76
+ ) {
77
+ const fromBefore2024 = node.creationTime < new Date('2024-01-01');
78
+
79
+ this.logger.error(
80
+ `Failed to decrypt public link node ${node.uid} (from before 2024: ${fromBefore2024})`,
81
+ error,
82
+ );
83
+
84
+ this.telemetry.recordMetric({
85
+ eventName: 'decryptionError',
86
+ volumeType: MetricVolumeType.SharedPublic,
87
+ field,
88
+ fromBefore2024,
89
+ error,
90
+ uid: node.uid,
91
+ });
92
+ }
93
+
94
+ reportVerificationError() {
95
+ // Authors or signatures are not provided on public links.
96
+ // We do not report any signature verification errors at this moment.
97
+ }
98
+ }
@@ -0,0 +1,41 @@
1
+ import { DriveCrypto } from '../../crypto';
2
+ import { ProtonDriveCryptoCache, ProtonDriveTelemetry, ProtonDriveAccount } from '../../interface';
3
+ import { DriveAPIService } from '../apiService';
4
+ import { SharingPublicAPIService } from './apiService';
5
+ import { SharingPublicCryptoCache } from './cryptoCache';
6
+ import { SharingPublicCryptoService } from './cryptoService';
7
+ import { SharingPublicManager } from './manager';
8
+
9
+ export { SharingPublicSessionManager } from './session/manager';
10
+
11
+ /**
12
+ * Provides facade for the whole sharing public module.
13
+ *
14
+ * The sharing public module is responsible for handling public link data, including
15
+ * API communication, encryption, decryption, and caching.
16
+ *
17
+ * This facade provides internal interface that other modules can use to
18
+ * interact with the public links.
19
+ */
20
+ export function initSharingPublicModule(
21
+ telemetry: ProtonDriveTelemetry,
22
+ apiService: DriveAPIService,
23
+ driveCryptoCache: ProtonDriveCryptoCache,
24
+ driveCrypto: DriveCrypto,
25
+ account: ProtonDriveAccount,
26
+ token: string,
27
+ password: string,
28
+ ) {
29
+ const api = new SharingPublicAPIService(telemetry.getLogger('sharingPublic-api'), apiService);
30
+ const cryptoCache = new SharingPublicCryptoCache(telemetry.getLogger('sharingPublic-crypto'), driveCryptoCache);
31
+ const cryptoService = new SharingPublicCryptoService(telemetry, driveCrypto, account, password);
32
+ const manager = new SharingPublicManager(
33
+ telemetry.getLogger('sharingPublic-nodes'),
34
+ api,
35
+ cryptoCache,
36
+ cryptoService,
37
+ token,
38
+ );
39
+
40
+ return manager;
41
+ }
@@ -0,0 +1,14 @@
1
+ // TODO: use them directly, or avoid them completely
2
+ export type {
3
+ EncryptedNode,
4
+ EncryptedNodeFolderCrypto,
5
+ EncryptedNodeFileCrypto,
6
+ DecryptedNode,
7
+ DecryptedNodeKeys,
8
+ } from '../nodes/interface';
9
+
10
+ export interface EncryptedShareCrypto {
11
+ base64UrlPasswordSalt: string;
12
+ armoredKey: string;
13
+ armoredPassphrase: string;
14
+ }