@protontech/drive-sdk 0.2.1 → 0.3.1

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 (165) hide show
  1. package/dist/crypto/interface.d.ts +5 -0
  2. package/dist/diagnostic/httpClient.d.ts +3 -3
  3. package/dist/diagnostic/interface.d.ts +26 -29
  4. package/dist/diagnostic/sdkDiagnostic.js +50 -24
  5. package/dist/diagnostic/sdkDiagnostic.js.map +1 -1
  6. package/dist/errors.d.ts +3 -3
  7. package/dist/errors.js +7 -7
  8. package/dist/errors.js.map +1 -1
  9. package/dist/interface/author.d.ts +1 -1
  10. package/dist/interface/events.d.ts +1 -1
  11. package/dist/interface/events.js.map +1 -1
  12. package/dist/interface/httpClient.d.ts +5 -5
  13. package/dist/interface/index.d.ts +15 -5
  14. package/dist/internal/apiService/apiService.js +12 -4
  15. package/dist/internal/apiService/apiService.js.map +1 -1
  16. package/dist/internal/apiService/errorCodes.d.ts +1 -0
  17. package/dist/internal/apiService/errorCodes.js.map +1 -1
  18. package/dist/internal/apiService/errors.d.ts +4 -3
  19. package/dist/internal/apiService/errors.js +7 -4
  20. package/dist/internal/apiService/errors.js.map +1 -1
  21. package/dist/internal/apiService/errors.test.js +2 -1
  22. package/dist/internal/apiService/errors.test.js.map +1 -1
  23. package/dist/internal/events/index.d.ts +1 -1
  24. package/dist/internal/nodes/apiService.js +3 -0
  25. package/dist/internal/nodes/apiService.js.map +1 -1
  26. package/dist/internal/nodes/apiService.test.js +18 -0
  27. package/dist/internal/nodes/apiService.test.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/cryptoService.js +44 -20
  33. package/dist/internal/nodes/cryptoService.js.map +1 -1
  34. package/dist/internal/nodes/nodesAccess.js +2 -2
  35. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  36. package/dist/internal/nodes/nodesManagement.js +0 -2
  37. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  38. package/dist/internal/shares/cryptoCache.d.ts +4 -3
  39. package/dist/internal/shares/cryptoCache.js +23 -6
  40. package/dist/internal/shares/cryptoCache.js.map +1 -1
  41. package/dist/internal/shares/cryptoCache.test.js +3 -2
  42. package/dist/internal/shares/cryptoCache.test.js.map +1 -1
  43. package/dist/internal/shares/index.js +1 -1
  44. package/dist/internal/shares/index.js.map +1 -1
  45. package/dist/internal/sharing/cryptoService.js +8 -6
  46. package/dist/internal/sharing/cryptoService.js.map +1 -1
  47. package/dist/internal/sharing/cryptoService.test.js +13 -0
  48. package/dist/internal/sharing/cryptoService.test.js.map +1 -1
  49. package/dist/internal/sharing/index.js +1 -1
  50. package/dist/internal/sharing/index.js.map +1 -1
  51. package/dist/internal/sharing/interface.d.ts +0 -4
  52. package/dist/internal/sharing/sharingAccess.d.ts +1 -0
  53. package/dist/internal/sharing/sharingAccess.js +6 -1
  54. package/dist/internal/sharing/sharingAccess.js.map +1 -1
  55. package/dist/internal/sharing/sharingAccess.test.js +3 -3
  56. package/dist/internal/sharing/sharingAccess.test.js.map +1 -1
  57. package/dist/internal/sharing/sharingManagement.d.ts +3 -1
  58. package/dist/internal/sharing/sharingManagement.js +37 -17
  59. package/dist/internal/sharing/sharingManagement.js.map +1 -1
  60. package/dist/internal/sharing/sharingManagement.test.js +61 -14
  61. package/dist/internal/sharing/sharingManagement.test.js.map +1 -1
  62. package/dist/internal/sharingPublic/apiService.d.ts +19 -0
  63. package/dist/internal/sharingPublic/apiService.js +134 -0
  64. package/dist/internal/sharingPublic/apiService.js.map +1 -0
  65. package/dist/internal/sharingPublic/cryptoCache.d.ts +19 -0
  66. package/dist/internal/sharingPublic/cryptoCache.js +72 -0
  67. package/dist/internal/sharingPublic/cryptoCache.js.map +1 -0
  68. package/dist/internal/sharingPublic/cryptoService.d.ts +23 -0
  69. package/dist/internal/sharingPublic/cryptoService.js +120 -0
  70. package/dist/internal/sharingPublic/cryptoService.js.map +1 -0
  71. package/dist/internal/sharingPublic/index.d.ts +15 -0
  72. package/dist/internal/sharingPublic/index.js +27 -0
  73. package/dist/internal/sharingPublic/index.js.map +1 -0
  74. package/dist/internal/sharingPublic/interface.d.ts +48 -0
  75. package/dist/internal/sharingPublic/interface.js +3 -0
  76. package/dist/internal/sharingPublic/interface.js.map +1 -0
  77. package/dist/internal/sharingPublic/manager.d.ts +19 -0
  78. package/dist/internal/sharingPublic/manager.js +79 -0
  79. package/dist/internal/sharingPublic/manager.js.map +1 -0
  80. package/dist/internal/sharingPublic/session/apiService.d.ts +28 -0
  81. package/dist/internal/sharingPublic/session/apiService.js +55 -0
  82. package/dist/internal/sharingPublic/session/apiService.js.map +1 -0
  83. package/dist/internal/sharingPublic/session/httpClient.d.ts +16 -0
  84. package/dist/internal/sharingPublic/session/httpClient.js +41 -0
  85. package/dist/internal/sharingPublic/session/httpClient.js.map +1 -0
  86. package/dist/internal/sharingPublic/session/index.d.ts +1 -0
  87. package/dist/internal/sharingPublic/session/index.js +6 -0
  88. package/dist/internal/sharingPublic/session/index.js.map +1 -0
  89. package/dist/internal/sharingPublic/session/interface.d.ts +18 -0
  90. package/dist/internal/sharingPublic/session/interface.js +3 -0
  91. package/dist/internal/sharingPublic/session/interface.js.map +1 -0
  92. package/dist/internal/sharingPublic/session/manager.d.ts +49 -0
  93. package/dist/internal/sharingPublic/session/manager.js +75 -0
  94. package/dist/internal/sharingPublic/session/manager.js.map +1 -0
  95. package/dist/internal/sharingPublic/session/session.d.ts +34 -0
  96. package/dist/internal/sharingPublic/session/session.js +67 -0
  97. package/dist/internal/sharingPublic/session/session.js.map +1 -0
  98. package/dist/internal/sharingPublic/session/url.d.ts +12 -0
  99. package/dist/internal/sharingPublic/session/url.js +23 -0
  100. package/dist/internal/sharingPublic/session/url.js.map +1 -0
  101. package/dist/internal/sharingPublic/session/url.test.d.ts +1 -0
  102. package/dist/internal/sharingPublic/session/url.test.js +59 -0
  103. package/dist/internal/sharingPublic/session/url.test.js.map +1 -0
  104. package/dist/internal/upload/manager.js +1 -3
  105. package/dist/internal/upload/manager.js.map +1 -1
  106. package/dist/internal/upload/manager.test.js +2 -2
  107. package/dist/internal/upload/manager.test.js.map +1 -1
  108. package/dist/protonDriveClient.d.ts +25 -10
  109. package/dist/protonDriveClient.js +44 -22
  110. package/dist/protonDriveClient.js.map +1 -1
  111. package/dist/protonDrivePublicLinkClient.d.ts +48 -0
  112. package/dist/protonDrivePublicLinkClient.js +71 -0
  113. package/dist/protonDrivePublicLinkClient.js.map +1 -0
  114. package/package.json +1 -1
  115. package/src/crypto/interface.ts +11 -0
  116. package/src/diagnostic/httpClient.ts +4 -4
  117. package/src/diagnostic/interface.ts +27 -29
  118. package/src/diagnostic/sdkDiagnostic.ts +58 -30
  119. package/src/errors.ts +5 -5
  120. package/src/interface/author.ts +1 -1
  121. package/src/interface/events.ts +1 -7
  122. package/src/interface/httpClient.ts +5 -5
  123. package/src/interface/index.ts +18 -6
  124. package/src/internal/apiService/apiService.ts +13 -4
  125. package/src/internal/apiService/errorCodes.ts +1 -0
  126. package/src/internal/apiService/errors.test.ts +2 -1
  127. package/src/internal/apiService/errors.ts +15 -4
  128. package/src/internal/events/index.ts +1 -1
  129. package/src/internal/nodes/apiService.test.ts +28 -0
  130. package/src/internal/nodes/apiService.ts +3 -0
  131. package/src/internal/nodes/cryptoCache.test.ts +4 -7
  132. package/src/internal/nodes/cryptoCache.ts +6 -7
  133. package/src/internal/nodes/cryptoService.ts +68 -34
  134. package/src/internal/nodes/interface.ts +2 -0
  135. package/src/internal/nodes/nodesAccess.ts +2 -2
  136. package/src/internal/nodes/nodesManagement.ts +0 -3
  137. package/src/internal/shares/cryptoCache.test.ts +3 -2
  138. package/src/internal/shares/cryptoCache.ts +26 -7
  139. package/src/internal/shares/index.ts +1 -1
  140. package/src/internal/sharing/cryptoService.test.ts +22 -1
  141. package/src/internal/sharing/cryptoService.ts +8 -6
  142. package/src/internal/sharing/index.ts +1 -0
  143. package/src/internal/sharing/interface.ts +0 -4
  144. package/src/internal/sharing/sharingAccess.test.ts +4 -4
  145. package/src/internal/sharing/sharingAccess.ts +6 -0
  146. package/src/internal/sharing/sharingManagement.test.ts +87 -24
  147. package/src/internal/sharing/sharingManagement.ts +56 -16
  148. package/src/internal/sharingPublic/apiService.ts +164 -0
  149. package/src/internal/sharingPublic/cryptoCache.ts +79 -0
  150. package/src/internal/sharingPublic/cryptoService.ts +162 -0
  151. package/src/internal/sharingPublic/index.ts +40 -0
  152. package/src/internal/sharingPublic/interface.ts +59 -0
  153. package/src/internal/sharingPublic/manager.ts +85 -0
  154. package/src/internal/sharingPublic/session/apiService.ts +74 -0
  155. package/src/internal/sharingPublic/session/httpClient.ts +48 -0
  156. package/src/internal/sharingPublic/session/index.ts +1 -0
  157. package/src/internal/sharingPublic/session/interface.ts +20 -0
  158. package/src/internal/sharingPublic/session/manager.ts +97 -0
  159. package/src/internal/sharingPublic/session/session.ts +78 -0
  160. package/src/internal/sharingPublic/session/url.test.ts +72 -0
  161. package/src/internal/sharingPublic/session/url.ts +23 -0
  162. package/src/internal/upload/manager.test.ts +2 -2
  163. package/src/internal/upload/manager.ts +2 -4
  164. package/src/protonDriveClient.ts +64 -27
  165. package/src/protonDrivePublicLinkClient.ts +121 -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,12 +59,16 @@ 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
- generateShareKeys: jest
62
- .fn()
63
- .mockResolvedValue({
64
- shareKey: { encrypted: 'encrypted-key', decrypted: { passphraseSessionKey: 'pass-session-key' } },
65
- }),
69
+ generateShareKeys: jest.fn().mockResolvedValue({
70
+ shareKey: { encrypted: 'encrypted-key', decrypted: { passphraseSessionKey: 'pass-session-key' } },
71
+ }),
66
72
  decryptShare: jest.fn().mockImplementation((share) => share),
67
73
  decryptInvitation: jest.fn().mockImplementation((invitation) => invitation),
68
74
  decryptExternalInvitation: jest.fn().mockImplementation((invitation) => invitation),
@@ -70,7 +76,7 @@ describe('SharingManagement', () => {
70
76
  encryptInvitation: jest.fn().mockImplementation(() => {}),
71
77
  encryptExternalInvitation: jest.fn().mockImplementation((invitation) => ({
72
78
  ...invitation,
73
- base64ExternalInvitationSignature: 'extenral-signature',
79
+ base64ExternalInvitationSignature: 'external-signature',
74
80
  })),
75
81
  decryptPublicLink: jest.fn().mockImplementation((publicLink) => publicLink),
76
82
  generatePublicLinkPassword: jest.fn().mockResolvedValue('generatedPassword'),
@@ -85,17 +91,12 @@ describe('SharingManagement', () => {
85
91
  };
86
92
  // @ts-expect-error No need to implement all methods for mocking
87
93
  sharesService = {
88
- loadEncryptedShare: jest
89
- .fn()
90
- .mockResolvedValue({
91
- id: 'shareId',
92
- addressId: 'addressId',
93
- creatorEmail: 'address@example.com',
94
- passphraseSessionKey: 'sharePassphraseSessionKey',
95
- }),
96
- getContextShareMemberEmailKey: jest
97
- .fn()
98
- .mockResolvedValue({ email: 'volume-email', addressId: 'addressId', addressKey: 'volume-key' }),
94
+ loadEncryptedShare: jest.fn().mockResolvedValue({
95
+ id: 'shareId',
96
+ addressId: 'addressId',
97
+ creatorEmail: 'address@example.com',
98
+ passphraseSessionKey: 'sharePassphraseSessionKey',
99
+ }),
99
100
  };
100
101
  // @ts-expect-error No need to implement all methods for mocking
101
102
  nodesService = {
@@ -111,6 +112,7 @@ describe('SharingManagement', () => {
111
112
  sharingManagement = new SharingManagement(
112
113
  getMockLogger(),
113
114
  apiService,
115
+ cache,
114
116
  cryptoService,
115
117
  accountService,
116
118
  sharesService,
@@ -196,13 +198,11 @@ describe('SharingManagement', () => {
196
198
  const nodeUid = 'volumeId~nodeUid';
197
199
 
198
200
  it('should create share if no exists', async () => {
199
- nodesService.getNode = jest
200
- .fn()
201
- .mockImplementation((nodeUid) => ({
202
- nodeUid,
203
- parentUid: 'parentUid',
204
- name: { ok: true, value: 'name' },
205
- }));
201
+ nodesService.getNode = jest.fn().mockImplementation((nodeUid) => ({
202
+ nodeUid,
203
+ parentUid: 'parentUid',
204
+ name: { ok: true, value: 'name' },
205
+ }));
206
206
  nodesService.notifyNodeChanged = jest.fn();
207
207
 
208
208
  const sharingInfo = await sharingManagement.shareNode(nodeUid, { users: ['email'] });
@@ -223,6 +223,7 @@ describe('SharingManagement', () => {
223
223
  expect(apiService.updateInvitation).not.toHaveBeenCalled();
224
224
  expect(apiService.inviteProtonUser).toHaveBeenCalled();
225
225
  expect(nodesService.notifyNodeChanged).toHaveBeenCalledWith(nodeUid);
226
+ expect(cache.addSharedByMeNodeUid).toHaveBeenCalledWith(nodeUid);
226
227
  });
227
228
  });
228
229
 
@@ -288,6 +289,7 @@ describe('SharingManagement', () => {
288
289
  });
289
290
  expect(apiService.updateInvitation).not.toHaveBeenCalled();
290
291
  expect(apiService.inviteProtonUser).toHaveBeenCalled();
292
+ expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
291
293
  });
292
294
 
293
295
  it('should share node with proton email with specific role', async () => {
@@ -311,6 +313,7 @@ describe('SharingManagement', () => {
311
313
  });
312
314
  expect(apiService.updateInvitation).not.toHaveBeenCalled();
313
315
  expect(apiService.inviteProtonUser).toHaveBeenCalled();
316
+ expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
314
317
  });
315
318
 
316
319
  it('should update existing role', async () => {
@@ -331,6 +334,7 @@ describe('SharingManagement', () => {
331
334
  });
332
335
  expect(apiService.updateInvitation).toHaveBeenCalled();
333
336
  expect(apiService.inviteProtonUser).not.toHaveBeenCalled();
337
+ expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
334
338
  });
335
339
 
336
340
  it('should be no-op if no change', async () => {
@@ -346,6 +350,25 @@ describe('SharingManagement', () => {
346
350
  });
347
351
  expect(apiService.updateInvitation).not.toHaveBeenCalled();
348
352
  expect(apiService.inviteProtonUser).not.toHaveBeenCalled();
353
+ expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
354
+ });
355
+
356
+ it('should use address from the root node context share', async () => {
357
+ nodesService.getRootNodeEmailKey = jest
358
+ .fn()
359
+ .mockResolvedValue({ email: 'my-volume-email', addressKey: 'my-volume-key' });
360
+
361
+ await sharingManagement.shareNode(nodeUid, { users: ['email'] });
362
+
363
+ expect(apiService.inviteProtonUser).toHaveBeenCalledWith(
364
+ 'shareId',
365
+ {
366
+ addedByEmail: 'my-volume-email',
367
+ inviteeEmail: 'email',
368
+ role: 'viewer',
369
+ },
370
+ expect.anything(),
371
+ );
349
372
  });
350
373
  });
351
374
 
@@ -374,6 +397,7 @@ describe('SharingManagement', () => {
374
397
  });
375
398
  expect(apiService.updateExternalInvitation).not.toHaveBeenCalled();
376
399
  expect(apiService.inviteExternalUser).toHaveBeenCalled();
400
+ expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
377
401
  });
378
402
 
379
403
  it('should share node with external email with specific role', async () => {
@@ -398,6 +422,7 @@ describe('SharingManagement', () => {
398
422
  });
399
423
  expect(apiService.updateExternalInvitation).not.toHaveBeenCalled();
400
424
  expect(apiService.inviteExternalUser).toHaveBeenCalled();
425
+ expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
401
426
  });
402
427
 
403
428
  it('should update existing role', async () => {
@@ -418,6 +443,7 @@ describe('SharingManagement', () => {
418
443
  });
419
444
  expect(apiService.updateExternalInvitation).toHaveBeenCalled();
420
445
  expect(apiService.inviteExternalUser).not.toHaveBeenCalled();
446
+ expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
421
447
  });
422
448
 
423
449
  it('should be no-op if no change', async () => {
@@ -433,6 +459,28 @@ describe('SharingManagement', () => {
433
459
  });
434
460
  expect(apiService.updateExternalInvitation).not.toHaveBeenCalled();
435
461
  expect(apiService.inviteExternalUser).not.toHaveBeenCalled();
462
+ expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
463
+ });
464
+
465
+ it('should use address from the root node context share', async () => {
466
+ nodesService.getRootNodeEmailKey = jest.fn().mockResolvedValue({
467
+ email: 'my-volume-email',
468
+ addressId: 'my-volume-addressId',
469
+ addressKey: 'my-volume-key',
470
+ });
471
+
472
+ await sharingManagement.shareNode(nodeUid, { users: ['email'] });
473
+
474
+ expect(apiService.inviteExternalUser).toHaveBeenCalledWith(
475
+ 'shareId',
476
+ {
477
+ inviterAddressId: 'my-volume-addressId',
478
+ inviteeEmail: 'email',
479
+ role: 'viewer',
480
+ base64Signature: 'external-signature',
481
+ },
482
+ expect.anything(),
483
+ );
436
484
  });
437
485
  });
438
486
 
@@ -482,6 +530,7 @@ describe('SharingManagement', () => {
482
530
  }),
483
531
  expect.anything(),
484
532
  );
533
+ expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
485
534
  });
486
535
  });
487
536
 
@@ -505,6 +554,7 @@ describe('SharingManagement', () => {
505
554
  expect(apiService.updateMember).toHaveBeenCalled();
506
555
  expect(apiService.updateInvitation).not.toHaveBeenCalled();
507
556
  expect(apiService.inviteProtonUser).not.toHaveBeenCalled();
557
+ expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
508
558
  });
509
559
 
510
560
  it('should be no-op if no change via proton user', async () => {
@@ -521,6 +571,7 @@ describe('SharingManagement', () => {
521
571
  expect(apiService.updateMember).not.toHaveBeenCalled();
522
572
  expect(apiService.updateInvitation).not.toHaveBeenCalled();
523
573
  expect(apiService.inviteProtonUser).not.toHaveBeenCalled();
574
+ expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
524
575
  });
525
576
 
526
577
  it('should update member via non-proton user', async () => {
@@ -542,6 +593,7 @@ describe('SharingManagement', () => {
542
593
  expect(apiService.updateMember).toHaveBeenCalled();
543
594
  expect(apiService.updateInvitation).not.toHaveBeenCalled();
544
595
  expect(apiService.inviteProtonUser).not.toHaveBeenCalled();
596
+ expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
545
597
  });
546
598
 
547
599
  it('should be no-op if no change via non-proton user', async () => {
@@ -558,6 +610,7 @@ describe('SharingManagement', () => {
558
610
  expect(apiService.updateMember).not.toHaveBeenCalled();
559
611
  expect(apiService.updateInvitation).not.toHaveBeenCalled();
560
612
  expect(apiService.inviteProtonUser).not.toHaveBeenCalled();
613
+ expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
561
614
  });
562
615
  });
563
616
 
@@ -605,6 +658,7 @@ describe('SharingManagement', () => {
605
658
  srp: 'publicLinkSrp',
606
659
  }),
607
660
  );
661
+ expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
608
662
  });
609
663
 
610
664
  it('should share node with custom password and expiration', async () => {
@@ -650,6 +704,7 @@ describe('SharingManagement', () => {
650
704
  srp: 'publicLinkSrp',
651
705
  }),
652
706
  );
707
+ expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
653
708
  });
654
709
 
655
710
  it('should update public link with custom password and expiration', async () => {
@@ -704,6 +759,7 @@ describe('SharingManagement', () => {
704
759
  srp: 'publicLinkSrp',
705
760
  }),
706
761
  );
762
+ expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
707
763
  });
708
764
 
709
765
  it('should not allow updating legacy public link', async () => {
@@ -809,6 +865,7 @@ describe('SharingManagement', () => {
809
865
  expect(apiService.deleteExternalInvitation).not.toHaveBeenCalled();
810
866
  expect(apiService.removeMember).not.toHaveBeenCalled();
811
867
  expect(apiService.removePublicLink).not.toHaveBeenCalled();
868
+ expect(cache.removeSharedByMeNodeUid).not.toHaveBeenCalled();
812
869
  });
813
870
 
814
871
  it('should delete external invitation', async () => {
@@ -825,6 +882,7 @@ describe('SharingManagement', () => {
825
882
  expect(apiService.deleteExternalInvitation).toHaveBeenCalled();
826
883
  expect(apiService.removeMember).not.toHaveBeenCalled();
827
884
  expect(apiService.removePublicLink).not.toHaveBeenCalled();
885
+ expect(cache.removeSharedByMeNodeUid).not.toHaveBeenCalled();
828
886
  });
829
887
 
830
888
  it('should remove member', async () => {
@@ -841,6 +899,7 @@ describe('SharingManagement', () => {
841
899
  expect(apiService.deleteExternalInvitation).not.toHaveBeenCalled();
842
900
  expect(apiService.removeMember).toHaveBeenCalled();
843
901
  expect(apiService.removePublicLink).not.toHaveBeenCalled();
902
+ expect(cache.removeSharedByMeNodeUid).not.toHaveBeenCalled();
844
903
  });
845
904
 
846
905
  it('should be no-op if not shared with email', async () => {
@@ -857,6 +916,7 @@ describe('SharingManagement', () => {
857
916
  expect(apiService.deleteExternalInvitation).not.toHaveBeenCalled();
858
917
  expect(apiService.removeMember).not.toHaveBeenCalled();
859
918
  expect(apiService.removePublicLink).not.toHaveBeenCalled();
919
+ expect(cache.removeSharedByMeNodeUid).not.toHaveBeenCalled();
860
920
  });
861
921
 
862
922
  it('should remove public link', async () => {
@@ -873,6 +933,7 @@ describe('SharingManagement', () => {
873
933
  expect(apiService.deleteExternalInvitation).not.toHaveBeenCalled();
874
934
  expect(apiService.removeMember).not.toHaveBeenCalled();
875
935
  expect(apiService.removePublicLink).toHaveBeenCalled();
936
+ expect(cache.removeSharedByMeNodeUid).not.toHaveBeenCalled();
876
937
  });
877
938
 
878
939
  it('should remove share if all is removed', async () => {
@@ -885,6 +946,7 @@ describe('SharingManagement', () => {
885
946
  expect(apiService.removeMember).not.toHaveBeenCalled();
886
947
  expect(apiService.removePublicLink).not.toHaveBeenCalled();
887
948
  expect(nodesService.notifyNodeChanged).toHaveBeenCalled();
949
+ expect(cache.removeSharedByMeNodeUid).toHaveBeenCalledWith(nodeUid);
888
950
  });
889
951
 
890
952
  it('should remove share if everything is manually removed', async () => {
@@ -899,6 +961,7 @@ describe('SharingManagement', () => {
899
961
  expect(apiService.deleteExternalInvitation).toHaveBeenCalled();
900
962
  expect(apiService.removeMember).toHaveBeenCalled();
901
963
  expect(apiService.removePublicLink).toHaveBeenCalled();
964
+ expect(cache.removeSharedByMeNodeUid).toHaveBeenCalledWith(nodeUid);
902
965
  });
903
966
  });
904
967
 
@@ -1,6 +1,6 @@
1
1
  import { c } from 'ttag';
2
2
 
3
- import { SessionKey } from '../../crypto';
3
+ import { PrivateKey, SessionKey } from '../../crypto';
4
4
  import { ValidationError } from '../../errors';
5
5
  import {
6
6
  Logger,
@@ -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;
@@ -33,6 +34,12 @@ interface Share {
33
34
  passphraseSessionKey: SessionKey;
34
35
  }
35
36
 
37
+ interface ContextShareAddress {
38
+ addressId: string;
39
+ addressKey: PrivateKey;
40
+ email: string;
41
+ }
42
+
36
43
  interface EmailOptions {
37
44
  message?: string;
38
45
  nodeName?: string;
@@ -48,6 +55,7 @@ export class SharingManagement {
48
55
  constructor(
49
56
  private logger: Logger,
50
57
  private apiService: SharingAPIService,
58
+ private cache: SharingCache,
51
59
  private cryptoService: SharingCryptoService,
52
60
  private account: ProtonDriveAccount,
53
61
  private sharesService: SharesService,
@@ -55,6 +63,7 @@ export class SharingManagement {
55
63
  ) {
56
64
  this.logger = logger;
57
65
  this.apiService = apiService;
66
+ this.cache = cache;
58
67
  this.cryptoService = cryptoService;
59
68
  this.account = account;
60
69
  this.sharesService = sharesService;
@@ -138,18 +147,22 @@ export class SharingManagement {
138
147
  throw new ValidationError(c('Error').t`Expiration date cannot be in the past`);
139
148
  }
140
149
 
150
+ let contextShareAddress: ContextShareAddress;
141
151
  let currentSharing = await this.getInternalSharingInfo(nodeUid);
142
- if (!currentSharing) {
152
+ if (currentSharing) {
153
+ contextShareAddress = await this.nodesService.getRootNodeEmailKey(nodeUid);
154
+ } else {
143
155
  const node = await this.nodesService.getNode(nodeUid);
144
- const share = await this.createShare(nodeUid);
156
+ const result = await this.createShare(nodeUid);
145
157
  currentSharing = {
146
- share,
158
+ share: result.share,
147
159
  nodeName: node.name.ok ? node.name.value : node.name.error.name,
148
160
  protonInvitations: [],
149
161
  nonProtonInvitations: [],
150
162
  members: [],
151
163
  publicLink: undefined,
152
164
  };
165
+ contextShareAddress = result.contextShareAddress;
153
166
  }
154
167
 
155
168
  const emailOptions: EmailOptions = {
@@ -187,7 +200,13 @@ export class SharingManagement {
187
200
  }
188
201
 
189
202
  this.logger.info(`Inviting user ${email} with role ${role} to node ${nodeUid}`);
190
- const invitation = await this.inviteProtonUser(currentSharing.share, email, role, emailOptions);
203
+ const invitation = await this.inviteProtonUser(
204
+ contextShareAddress,
205
+ currentSharing.share,
206
+ email,
207
+ role,
208
+ emailOptions,
209
+ );
191
210
  currentSharing.protonInvitations.push(invitation);
192
211
  }
193
212
 
@@ -225,7 +244,13 @@ export class SharingManagement {
225
244
  }
226
245
 
227
246
  this.logger.info(`Inviting external user ${email} with role ${role} to node ${nodeUid}`);
228
- const invitation = await this.inviteExternalUser(currentSharing.share, email, role, emailOptions);
247
+ const invitation = await this.inviteExternalUser(
248
+ contextShareAddress,
249
+ currentSharing.share,
250
+ email,
251
+ role,
252
+ emailOptions,
253
+ );
229
254
  currentSharing.nonProtonInvitations.push(invitation);
230
255
  }
231
256
 
@@ -241,7 +266,7 @@ export class SharingManagement {
241
266
  );
242
267
  } else {
243
268
  this.logger.info(`Sharing via public link with role ${options.role} to node ${nodeUid}`);
244
- currentSharing.publicLink = await this.shareViaLink(currentSharing.share, options);
269
+ currentSharing.publicLink = await this.shareViaLink(contextShareAddress, currentSharing.share, options);
245
270
  }
246
271
  }
247
272
 
@@ -371,7 +396,7 @@ export class SharingManagement {
371
396
  };
372
397
  }
373
398
 
374
- private async createShare(nodeUid: string): Promise<Share> {
399
+ private async createShare(nodeUid: string): Promise<{ share: Share; contextShareAddress: ContextShareAddress }> {
375
400
  const node = await this.nodesService.getNode(nodeUid);
376
401
  if (!node.parentUid) {
377
402
  throw new ValidationError(c('Error').t`Cannot share root folder`);
@@ -387,26 +412,42 @@ export class SharingManagement {
387
412
  base64NameKeyPacket: keys.base64NameKeyPacket,
388
413
  });
389
414
  await this.nodesService.notifyNodeChanged(nodeUid);
390
- return {
415
+ if (await this.cache.hasSharedByMeNodeUidsLoaded()) {
416
+ await this.cache.addSharedByMeNodeUid(nodeUid);
417
+ }
418
+
419
+ const share = {
391
420
  volumeId,
392
421
  shareId,
393
422
  creatorEmail: email,
394
423
  passphraseSessionKey: keys.shareKey.decrypted.passphraseSessionKey,
395
424
  };
425
+ const contextShareAddress = {
426
+ email,
427
+ addressId,
428
+ addressKey,
429
+ };
430
+ return {
431
+ share,
432
+ contextShareAddress,
433
+ };
396
434
  }
397
435
 
398
436
  private async deleteShare(shareId: string, nodeUid: string): Promise<void> {
399
437
  await this.apiService.deleteShare(shareId);
400
438
  await this.nodesService.notifyNodeChanged(nodeUid);
439
+ if (await this.cache.hasSharedByMeNodeUidsLoaded()) {
440
+ await this.cache.removeSharedByMeNodeUid(nodeUid);
441
+ }
401
442
  }
402
443
 
403
444
  private async inviteProtonUser(
445
+ inviter: ContextShareAddress,
404
446
  share: Share,
405
447
  inviteeEmail: string,
406
448
  role: MemberRole,
407
449
  emailOptions: EmailOptions,
408
450
  ): Promise<ProtonInvitation> {
409
- const inviter = await this.sharesService.getContextShareMemberEmailKey(share.shareId);
410
451
  const invitationCrypto = await this.cryptoService.encryptInvitation(
411
452
  share.passphraseSessionKey,
412
453
  inviter.addressKey,
@@ -461,12 +502,12 @@ export class SharingManagement {
461
502
  }
462
503
 
463
504
  private async inviteExternalUser(
505
+ inviter: ContextShareAddress,
464
506
  share: Share,
465
507
  inviteeEmail: string,
466
508
  role: MemberRole,
467
509
  emailOptions: EmailOptions,
468
510
  ): Promise<NonProtonInvitation> {
469
- const inviter = await this.sharesService.getContextShareMemberEmailKey(share.shareId);
470
511
  const invitationCrypto = await this.cryptoService.encryptExternalInvitation(
471
512
  share.passphraseSessionKey,
472
513
  inviter.addressKey,
@@ -515,21 +556,20 @@ export class SharingManagement {
515
556
  }
516
557
 
517
558
  private async shareViaLink(
559
+ inviter: ContextShareAddress,
518
560
  share: Share,
519
561
  options: SharePublicLinkSettingsObject,
520
562
  ): Promise<PublicLinkWithCreatorEmail> {
521
- const { email: creatorEmail } = await this.sharesService.getContextShareMemberEmailKey(share.shareId);
522
-
523
563
  const generatedPassword = await this.cryptoService.generatePublicLinkPassword();
524
564
  const password = options.customPassword ? `${generatedPassword}${options.customPassword}` : generatedPassword;
525
565
 
526
566
  const { crypto, srp } = await this.cryptoService.encryptPublicLink(
527
- creatorEmail,
567
+ inviter.email,
528
568
  share.passphraseSessionKey,
529
569
  password,
530
570
  );
531
571
  const publicLink = await this.apiService.createPublicLink(share.shareId, {
532
- creatorEmail,
572
+ creatorEmail: inviter.email,
533
573
  role: options.role,
534
574
  includesCustomPassword: !!options.customPassword,
535
575
  expirationTime: options.expiration ? Math.floor(options.expiration.getTime() / 1000) : undefined,
@@ -545,7 +585,7 @@ export class SharingManagement {
545
585
  customPassword: options.customPassword,
546
586
  expirationTime: options.expiration,
547
587
  numberOfInitializedDownloads: 0,
548
- creatorEmail,
588
+ creatorEmail: inviter.email,
549
589
  };
550
590
  }
551
591
 
@@ -0,0 +1,164 @@
1
+ import { DriveAPIService, drivePaths, nodeTypeNumberToNodeType } from '../apiService';
2
+ import { Logger } 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 *iterateChildren(parentUid: string): 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
+ );
53
+
54
+ for (const link of response.Links) {
55
+ yield linkToEncryptedNode(this.logger, token, link);
56
+ }
57
+
58
+ if (response.Links.length < PAGE_SIZE) {
59
+ break;
60
+ }
61
+ page++;
62
+ }
63
+ }
64
+ }
65
+
66
+ function tokenToEncryptedNode(logger: Logger, token: GetTokenInfoResponse['Token']): EncryptedNode {
67
+ const baseNodeMetadata = {
68
+ // Internal metadata
69
+ encryptedName: token.Name,
70
+
71
+ // Basic node metadata
72
+ uid: makeNodeUid(token.Token, token.LinkID),
73
+ parentUid: undefined,
74
+ type: nodeTypeNumberToNodeType(logger, token.LinkType),
75
+ };
76
+
77
+ const baseCryptoNodeMetadata = {
78
+ signatureEmail: token.SignatureEmail || undefined,
79
+ armoredKey: token.NodeKey,
80
+ armoredNodePassphrase: token.NodePassphrase,
81
+ armoredNodePassphraseSignature: token.NodePassphraseSignature || undefined,
82
+ };
83
+
84
+ if (token.LinkType === 1 && token.NodeHashKey) {
85
+ return {
86
+ ...baseNodeMetadata,
87
+ encryptedCrypto: {
88
+ ...baseCryptoNodeMetadata,
89
+ folder: {
90
+ armoredHashKey: token.NodeHashKey as string,
91
+ },
92
+ },
93
+ };
94
+ }
95
+
96
+ if (token.LinkType === 2 && token.ContentKeyPacket) {
97
+ return {
98
+ ...baseNodeMetadata,
99
+ totalStorageSize: token.Size || undefined,
100
+ mediaType: token.MIMEType || undefined,
101
+ encryptedCrypto: {
102
+ ...baseCryptoNodeMetadata,
103
+ file: {
104
+ base64ContentKeyPacket: token.ContentKeyPacket,
105
+ },
106
+ },
107
+ };
108
+ }
109
+
110
+ throw new Error(`Unknown node type: ${token.LinkType}`);
111
+ }
112
+
113
+ function linkToEncryptedNode(
114
+ logger: Logger,
115
+ token: string,
116
+ link: GetTokenFolderChildrenResponse['Links'][0],
117
+ ): EncryptedNode {
118
+ const baseNodeMetadata = {
119
+ // Internal metadata
120
+ hash: link.Hash || undefined,
121
+ encryptedName: link.Name,
122
+
123
+ // Basic node metadata
124
+ uid: makeNodeUid(token, link.LinkID),
125
+ parentUid: link.ParentLinkID ? makeNodeUid(token, link.ParentLinkID) : undefined,
126
+ type: nodeTypeNumberToNodeType(logger, link.Type),
127
+ totalStorageSize: link.TotalSize,
128
+ };
129
+
130
+ const baseCryptoNodeMetadata = {
131
+ signatureEmail: link.SignatureEmail || undefined,
132
+ armoredKey: link.NodeKey,
133
+ armoredNodePassphrase: link.NodePassphrase,
134
+ armoredNodePassphraseSignature: link.NodePassphraseSignature || undefined,
135
+ };
136
+
137
+ if (link.Type === 1 && link.FolderProperties) {
138
+ return {
139
+ ...baseNodeMetadata,
140
+ encryptedCrypto: {
141
+ ...baseCryptoNodeMetadata,
142
+ folder: {
143
+ armoredHashKey: link.FolderProperties.NodeHashKey as string,
144
+ },
145
+ },
146
+ };
147
+ }
148
+
149
+ if (link.Type === 2 && link.FileProperties?.ContentKeyPacket) {
150
+ return {
151
+ ...baseNodeMetadata,
152
+ totalStorageSize: link.FileProperties.ActiveRevision?.Size || undefined,
153
+ mediaType: link.MIMEType || undefined,
154
+ encryptedCrypto: {
155
+ ...baseCryptoNodeMetadata,
156
+ file: {
157
+ base64ContentKeyPacket: link.FileProperties.ContentKeyPacket,
158
+ },
159
+ },
160
+ };
161
+ }
162
+
163
+ throw new Error(`Unknown node type: ${link.Type}`);
164
+ }