@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
@@ -37,6 +37,7 @@ import {
37
37
  EncryptedBookmark,
38
38
  SharesService,
39
39
  } from './interface';
40
+ import { DecryptionError } from '../../errors';
40
41
 
41
42
  // Version 2 of bcrypt with 2**10 rounds.
42
43
  // https://en.wikipedia.org/wiki/Bcrypt#Description
@@ -425,10 +426,11 @@ export class SharingCryptoService {
425
426
  // TODO: Signatures are not checked and not specified in the interface.
426
427
  // In the future, we will need to add authorship verification.
427
428
 
429
+ let password: string;
428
430
  let urlPassword: string;
429
431
  let customPassword: Result<string | undefined, Error>;
430
432
  try {
431
- const password = await this.decryptBookmarkUrlPassword(encryptedBookmark);
433
+ password = await this.decryptBookmarkUrlPassword(encryptedBookmark);
432
434
  const result = splitGeneratedAndCustomPassword(password);
433
435
  urlPassword = result.password;
434
436
  customPassword = resultOk(result.customPassword);
@@ -446,7 +448,7 @@ export class SharingCryptoService {
446
448
 
447
449
  let shareKey: PrivateKey;
448
450
  try {
449
- shareKey = await this.decryptBookmarkKey(encryptedBookmark, urlPassword);
451
+ shareKey = await this.decryptBookmarkKey(encryptedBookmark, password);
450
452
  } catch (originalError: unknown) {
451
453
  const error = originalError instanceof Error ? originalError : new Error(c('Error').t`Unknown error`);
452
454
  return {
@@ -493,15 +495,15 @@ export class SharingCryptoService {
493
495
 
494
496
  const message = getErrorMessage(error);
495
497
  const errorMessage = c('Error').t`Failed to decrypt bookmark password: ${message}`;
496
- throw new Error(errorMessage);
498
+ throw new DecryptionError(errorMessage, { cause: error });
497
499
  }
498
500
  }
499
501
 
500
- private async decryptBookmarkKey(encryptedBookmark: EncryptedBookmark, urlPassword: string): Promise<PrivateKey> {
502
+ private async decryptBookmarkKey(encryptedBookmark: EncryptedBookmark, password: string): Promise<PrivateKey> {
501
503
  try {
502
504
  // Use the password to decrypt the share key.
503
505
  const { key: shareKey } = await this.driveCrypto.decryptKeyWithSrpPassword(
504
- urlPassword,
506
+ password,
505
507
  encryptedBookmark.url.base64SharePasswordSalt,
506
508
  encryptedBookmark.share.armoredKey,
507
509
  encryptedBookmark.share.armoredPassphrase,
@@ -519,7 +521,7 @@ export class SharingCryptoService {
519
521
 
520
522
  const message = getErrorMessage(error);
521
523
  const errorMessage = c('Error').t`Failed to decrypt bookmark key: ${message}`;
522
- throw new Error(errorMessage);
524
+ throw new DecryptionError(errorMessage, { cause: error });
523
525
  }
524
526
  }
525
527
 
@@ -32,6 +32,7 @@ export function initSharingModule(
32
32
  const sharingManagement = new SharingManagement(
33
33
  telemetry.getLogger('sharing'),
34
34
  api,
35
+ cache,
35
36
  cryptoService,
36
37
  account,
37
38
  sharesService,
@@ -5,7 +5,7 @@ import { DecryptedNode } from '../nodes';
5
5
 
6
6
  export enum SharingType {
7
7
  SharedByMe = 'sharedByMe',
8
- sharedWithMe = 'sharedWithMe',
8
+ SharedWithMe = 'sharedWithMe',
9
9
  }
10
10
 
11
11
  /**
@@ -1,4 +1,6 @@
1
- import { NodeType, resultError, resultOk } from '../../interface';
1
+ import { getMockLogger } from '../../tests/logger';
2
+ import { NodeType, resultError, resultOk, MemberRole } from '../../interface';
3
+ import { ValidationError } from '../../errors';
2
4
  import { SharingAPIService } from './apiService';
3
5
  import { SharingCache } from './cache';
4
6
  import { SharingCryptoService } from './cryptoService';
@@ -14,8 +16,12 @@ describe('SharingAccess', () => {
14
16
 
15
17
  let sharingAccess: SharingAccess;
16
18
 
17
- const nodeUids = Array.from({ length: BATCH_LOADING_SIZE + 5 }, (_, i) => `nodeUid${i}`);
18
- const nodes = nodeUids.map((nodeUid) => ({ nodeUid }));
19
+ const nodeUids = Array.from({ length: BATCH_LOADING_SIZE + 5 }, (_, i) => `volumeId~nodeUid${i}`);
20
+ const nodes = nodeUids.map((nodeUid) => ({
21
+ nodeUid,
22
+ shareId: 'shareId',
23
+ name: { ok: true, value: `name${nodeUid.split('~')[1]}` }
24
+ }));
19
25
  const nodeUidsIterator = async function* () {
20
26
  for (const nodeUid of nodeUids) {
21
27
  yield nodeUid;
@@ -33,25 +39,67 @@ describe('SharingAccess', () => {
33
39
  creationTime: new Date('2025-01-01'),
34
40
  node: {
35
41
  type: NodeType.File,
36
- mediaType: 'mediaType',
42
+ mediaType: 'image/jpeg',
37
43
  },
38
44
  };
39
45
  }),
46
+ removeMember: jest.fn(),
47
+ iterateInvitationUids: jest.fn().mockImplementation(async function* () {
48
+ yield 'invitationUid';
49
+ }),
50
+ getInvitation: jest.fn().mockResolvedValue({
51
+ uid: 'invitationUid',
52
+ node: { uid: 'volumeId~nodeUid' },
53
+ inviteeEmail: 'invitee-email',
54
+ role: MemberRole.Viewer,
55
+ }),
56
+ acceptInvitation: jest.fn(),
57
+ rejectInvitation: jest.fn(),
58
+ deleteBookmark: jest.fn(),
40
59
  };
60
+
41
61
  // @ts-expect-error No need to implement all methods for mocking
42
62
  cache = {
43
63
  setSharedByMeNodeUids: jest.fn(),
44
64
  setSharedWithMeNodeUids: jest.fn(),
65
+ getSharedByMeNodeUids: jest.fn(),
66
+ getSharedWithMeNodeUids: jest.fn(),
67
+ hasSharedByMeNodeUidsLoaded: jest.fn().mockResolvedValue(true),
68
+ hasSharedWithMeNodeUidsLoaded: jest.fn().mockResolvedValue(true),
69
+ addSharedByMeNodeUid: jest.fn(),
70
+ removeSharedByMeNodeUid: jest.fn(),
71
+ addSharedWithMeNodeUid: jest.fn(),
72
+ removeSharedWithMeNodeUid: jest.fn(),
45
73
  };
74
+
46
75
  // @ts-expect-error No need to implement all methods for mocking
47
76
  cryptoService = {
48
77
  decryptInvitation: jest.fn(),
49
78
  decryptBookmark: jest.fn(),
79
+ decryptInvitationWithNode: jest.fn().mockResolvedValue({
80
+ uid: 'invitationUid',
81
+ inviteeEmail: 'invitee-email',
82
+ role: MemberRole.Viewer,
83
+ node: {
84
+ uid: 'volumeId~nodeUid',
85
+ name: { ok: true, value: 'SharedFile.txt' },
86
+ type: NodeType.File,
87
+ },
88
+ }),
89
+ acceptInvitation: jest.fn().mockResolvedValue({
90
+ base64SessionKeySignature: 'mockSignature',
91
+ }),
50
92
  };
93
+
51
94
  // @ts-expect-error No need to implement all methods for mocking
52
95
  sharesService = {
53
96
  getMyFilesIDs: jest.fn().mockResolvedValue({ volumeId: 'volumeId' }),
97
+ loadEncryptedShare: jest.fn().mockResolvedValue({
98
+ id: 'shareId',
99
+ membership: { memberUid: 'memberUid' },
100
+ }),
54
101
  };
102
+
55
103
  // @ts-expect-error No need to implement all methods for mocking
56
104
  nodesService = {
57
105
  iterateNodes: jest.fn().mockImplementation(async function* (nodeUids) {
@@ -61,13 +109,18 @@ describe('SharingAccess', () => {
61
109
  }
62
110
  }
63
111
  }),
112
+ getNode: jest.fn().mockResolvedValue({
113
+ nodeUid: 'volumeId~nodeUid',
114
+ shareId: 'shareId',
115
+ name: { ok: true, value: 'TestFile.txt' },
116
+ }),
64
117
  };
65
118
 
66
119
  sharingAccess = new SharingAccess(apiService, cache, cryptoService, sharesService, nodesService);
67
120
  });
68
121
 
69
122
  describe('iterateSharedNodes', () => {
70
- it('should iterate from cache', async () => {
123
+ it('should iterate from cache when available', async () => {
71
124
  cache.getSharedByMeNodeUids = jest.fn().mockResolvedValue(nodeUids);
72
125
 
73
126
  const result = await Array.fromAsync(sharingAccess.iterateSharedNodes());
@@ -77,20 +130,32 @@ describe('SharingAccess', () => {
77
130
  expect(cache.setSharedByMeNodeUids).not.toHaveBeenCalled();
78
131
  });
79
132
 
80
- it('should iterate from API', async () => {
81
- cache.getSharedByMeNodeUids = jest.fn().mockRejectedValue(new Error('Not cached'));
133
+ it('should iterate from API when cache is empty', async () => {
134
+ cache.getSharedByMeNodeUids = jest.fn().mockRejectedValue(new Error('Cache miss'));
82
135
 
83
136
  const result = await Array.fromAsync(sharingAccess.iterateSharedNodes());
84
137
 
85
138
  expect(result).toEqual(nodes);
86
139
  expect(apiService.iterateSharedNodeUids).toHaveBeenCalledWith('volumeId', undefined);
87
- expect(nodesService.iterateNodes).toHaveBeenCalledTimes(2); // Mocked is a bit more over one batch
140
+ expect(nodesService.iterateNodes).toHaveBeenCalledTimes(2);
88
141
  expect(cache.setSharedByMeNodeUids).toHaveBeenCalledWith(nodeUids);
89
142
  });
143
+
144
+ it('should ignore missing nodes during iteration', async () => {
145
+ cache.getSharedByMeNodeUids = jest.fn().mockResolvedValue(['volumeId~nodeUid1', 'volumeId~missingNode']);
146
+ nodesService.iterateNodes = jest.fn().mockImplementation(async function* () {
147
+ yield { nodeUid: 'volumeId~nodeUid1', name: { ok: true, value: 'file1.txt' } };
148
+ yield { missingUid: 'volumeId~missingNode' };
149
+ });
150
+
151
+ const result = await Array.fromAsync(sharingAccess.iterateSharedNodes());
152
+
153
+ expect(result).toEqual([{ nodeUid: 'volumeId~nodeUid1', name: { ok: true, value: 'file1.txt' } }]);
154
+ });
90
155
  });
91
156
 
92
157
  describe('iterateSharedNodesWithMe', () => {
93
- it('should iterate from cache', async () => {
158
+ it('should iterate from cache when available', async () => {
94
159
  cache.getSharedWithMeNodeUids = jest.fn().mockResolvedValue(nodeUids);
95
160
 
96
161
  const result = await Array.fromAsync(sharingAccess.iterateSharedNodesWithMe());
@@ -100,24 +165,149 @@ describe('SharingAccess', () => {
100
165
  expect(cache.setSharedWithMeNodeUids).not.toHaveBeenCalled();
101
166
  });
102
167
 
103
- it('should iterate from API', async () => {
104
- cache.getSharedWithMeNodeUids = jest.fn().mockRejectedValue(new Error('Not cached'));
168
+ it('should iterate from API when cache is empty', async () => {
169
+ cache.getSharedWithMeNodeUids = jest.fn().mockRejectedValue(new Error('Cache miss'));
105
170
 
106
171
  const result = await Array.fromAsync(sharingAccess.iterateSharedNodesWithMe());
107
172
 
108
173
  expect(result).toEqual(nodes);
109
174
  expect(apiService.iterateSharedWithMeNodeUids).toHaveBeenCalledWith(undefined);
110
- expect(nodesService.iterateNodes).toHaveBeenCalledTimes(2); // Mocked is a bit more over one batch
175
+ expect(nodesService.iterateNodes).toHaveBeenCalledTimes(2);
111
176
  expect(cache.setSharedWithMeNodeUids).toHaveBeenCalledWith(nodeUids);
112
177
  });
113
178
  });
114
179
 
180
+ describe('removeSharedNodeWithMe', () => {
181
+ const nodeUid = 'volumeId~nodeUid';
182
+
183
+ it('should remove member and update cache', async () => {
184
+ await sharingAccess.removeSharedNodeWithMe(nodeUid);
185
+
186
+ expect(nodesService.getNode).toHaveBeenCalledWith(nodeUid);
187
+ expect(sharesService.loadEncryptedShare).toHaveBeenCalledWith('shareId');
188
+ expect(apiService.removeMember).toHaveBeenCalledWith('memberUid');
189
+ expect(cache.removeSharedWithMeNodeUid).toHaveBeenCalledWith(nodeUid);
190
+ });
191
+
192
+ it('should return early if node is not shared', async () => {
193
+ nodesService.getNode = jest.fn().mockResolvedValue({
194
+ nodeUid,
195
+ shareId: undefined,
196
+ name: { ok: true, value: 'UnsharedFile.txt' }
197
+ });
198
+
199
+ await sharingAccess.removeSharedNodeWithMe(nodeUid);
200
+
201
+ expect(sharesService.loadEncryptedShare).not.toHaveBeenCalled();
202
+ expect(apiService.removeMember).not.toHaveBeenCalled();
203
+ expect(cache.removeSharedWithMeNodeUid).not.toHaveBeenCalled();
204
+ });
205
+
206
+ it('should throw ValidationError if no membership found', async () => {
207
+ sharesService.loadEncryptedShare = jest.fn().mockResolvedValue({
208
+ id: 'shareId',
209
+ membership: undefined,
210
+ });
211
+
212
+ await expect(sharingAccess.removeSharedNodeWithMe(nodeUid)).rejects.toThrow(ValidationError);
213
+ expect(apiService.removeMember).not.toHaveBeenCalled();
214
+ expect(cache.removeSharedWithMeNodeUid).not.toHaveBeenCalled();
215
+ });
216
+ });
217
+
218
+ describe('iterateInvitations', () => {
219
+ it('should iterate and decrypt invitations', async () => {
220
+ const result = await Array.fromAsync(sharingAccess.iterateInvitations());
221
+
222
+ expect(result).toEqual([{
223
+ uid: 'invitationUid',
224
+ inviteeEmail: 'invitee-email',
225
+ role: MemberRole.Viewer,
226
+ node: {
227
+ uid: 'volumeId~nodeUid',
228
+ name: { ok: true, value: 'SharedFile.txt' },
229
+ type: NodeType.File,
230
+ },
231
+ }]);
232
+ expect(apiService.iterateInvitationUids).toHaveBeenCalledWith(undefined);
233
+ expect(apiService.getInvitation).toHaveBeenCalledWith('invitationUid');
234
+ expect(cryptoService.decryptInvitationWithNode).toHaveBeenCalledWith({
235
+ uid: 'invitationUid',
236
+ node: { uid: 'volumeId~nodeUid' },
237
+ inviteeEmail: 'invitee-email',
238
+ role: MemberRole.Viewer,
239
+ });
240
+ });
241
+ });
242
+
243
+ describe('acceptInvitation', () => {
244
+ it('should accept invitation and update cache', async () => {
245
+ const invitationUid = 'invitationUid';
246
+
247
+ await sharingAccess.acceptInvitation(invitationUid);
248
+
249
+ expect(apiService.getInvitation).toHaveBeenCalledWith(invitationUid);
250
+ expect(cryptoService.acceptInvitation).toHaveBeenCalledWith({
251
+ uid: 'invitationUid',
252
+ node: { uid: 'volumeId~nodeUid' },
253
+ inviteeEmail: 'invitee-email',
254
+ role: MemberRole.Viewer,
255
+ });
256
+ expect(apiService.acceptInvitation).toHaveBeenCalledWith(invitationUid, 'mockSignature');
257
+ expect(cache.addSharedWithMeNodeUid).toHaveBeenCalledWith('volumeId~nodeUid');
258
+ });
259
+
260
+ it('should not update cache when not loaded', async () => {
261
+ const invitationUid = 'invitationUid';
262
+ cache.hasSharedWithMeNodeUidsLoaded = jest.fn().mockResolvedValue(false);
263
+
264
+ await sharingAccess.acceptInvitation(invitationUid);
265
+
266
+ expect(apiService.acceptInvitation).toHaveBeenCalledWith(invitationUid, 'mockSignature');
267
+ expect(cache.addSharedWithMeNodeUid).not.toHaveBeenCalled();
268
+ });
269
+ });
270
+
271
+ describe('rejectInvitation', () => {
272
+ it('should reject invitation', async () => {
273
+ const invitationUid = 'invitationUid';
274
+
275
+ await sharingAccess.rejectInvitation(invitationUid);
276
+
277
+ expect(apiService.rejectInvitation).toHaveBeenCalledWith(invitationUid);
278
+ });
279
+ });
280
+
115
281
  describe('iterateBookmarks', () => {
116
- it('should return decrypted bookmark', async () => {
282
+ it('should return successfully decrypted bookmark', async () => {
283
+ cryptoService.decryptBookmark = jest.fn().mockResolvedValue({
284
+ url: resultOk('https://example.com/file.pdf'),
285
+ customPassword: resultOk('password123'),
286
+ nodeName: resultOk('ImportantDocument.pdf'),
287
+ });
288
+
289
+ const result = await Array.fromAsync(sharingAccess.iterateBookmarks());
290
+
291
+ expect(result).toEqual([
292
+ resultOk({
293
+ uid: 'tokenId',
294
+ creationTime: new Date('2025-01-01'),
295
+ url: 'https://example.com/file.pdf',
296
+ customPassword: 'password123',
297
+ node: {
298
+ name: 'ImportantDocument.pdf',
299
+ type: NodeType.File,
300
+ mediaType: 'image/jpeg',
301
+ },
302
+ }),
303
+ ]);
304
+ });
305
+
306
+ it('should return successfully decrypted bookmark with undefined password', async () => {
117
307
  cryptoService.decryptBookmark = jest.fn().mockResolvedValue({
118
- url: resultOk('url'),
119
- customPassword: resultOk('customPassword'),
120
- nodeName: resultOk('nodeName'),
308
+ url: resultOk('https://example.com/file.pdf'),
309
+ customPassword: resultOk(undefined),
310
+ nodeName: resultOk('PublicDocument.pdf'),
121
311
  });
122
312
 
123
313
  const result = await Array.fromAsync(sharingAccess.iterateBookmarks());
@@ -126,22 +316,46 @@ describe('SharingAccess', () => {
126
316
  resultOk({
127
317
  uid: 'tokenId',
128
318
  creationTime: new Date('2025-01-01'),
129
- url: 'url',
130
- customPassword: 'customPassword',
319
+ url: 'https://example.com/file.pdf',
320
+ customPassword: undefined,
321
+ node: {
322
+ name: 'PublicDocument.pdf',
323
+ type: NodeType.File,
324
+ mediaType: 'image/jpeg',
325
+ },
326
+ }),
327
+ ]);
328
+ });
329
+
330
+ it('should return degraded bookmark when URL cannot be decrypted', async () => {
331
+ cryptoService.decryptBookmark = jest.fn().mockResolvedValue({
332
+ url: resultError('URL decryption failed'),
333
+ customPassword: resultOk('password123'),
334
+ nodeName: resultOk('Document.pdf'),
335
+ });
336
+
337
+ const result = await Array.fromAsync(sharingAccess.iterateBookmarks());
338
+
339
+ expect(result).toEqual([
340
+ resultError({
341
+ uid: 'tokenId',
342
+ creationTime: new Date('2025-01-01'),
343
+ url: resultError('URL decryption failed'),
344
+ customPassword: resultOk('password123'),
131
345
  node: {
132
- name: 'nodeName',
346
+ name: resultOk('Document.pdf'),
133
347
  type: NodeType.File,
134
- mediaType: 'mediaType',
348
+ mediaType: 'image/jpeg',
135
349
  },
136
350
  }),
137
351
  ]);
138
352
  });
139
353
 
140
- it('should return degraded bookmark if URL password cannot be decrypted', async () => {
354
+ it('should return degraded bookmark when custom password cannot be decrypted', async () => {
141
355
  cryptoService.decryptBookmark = jest.fn().mockResolvedValue({
142
- url: resultError('url cannot be decrypted'),
143
- customPassword: resultOk('url cannot be decrypted'),
144
- nodeName: resultError('url cannot be decrypted'),
356
+ url: resultOk('https://example.com/file.pdf'),
357
+ customPassword: resultError('Password decryption failed'),
358
+ nodeName: resultOk('Document.pdf'),
145
359
  });
146
360
 
147
361
  const result = await Array.fromAsync(sharingAccess.iterateBookmarks());
@@ -150,22 +364,22 @@ describe('SharingAccess', () => {
150
364
  resultError({
151
365
  uid: 'tokenId',
152
366
  creationTime: new Date('2025-01-01'),
153
- url: resultError('url cannot be decrypted'),
154
- customPassword: resultOk('url cannot be decrypted'),
367
+ url: resultOk('https://example.com/file.pdf'),
368
+ customPassword: resultError('Password decryption failed'),
155
369
  node: {
156
- name: resultError('url cannot be decrypted'),
370
+ name: resultOk('Document.pdf'),
157
371
  type: NodeType.File,
158
- mediaType: 'mediaType',
372
+ mediaType: 'image/jpeg',
159
373
  },
160
374
  }),
161
375
  ]);
162
376
  });
163
377
 
164
- it('should return degraded bookmark if node name cannot be decrypted', async () => {
378
+ it('should return degraded bookmark when node name cannot be decrypted', async () => {
165
379
  cryptoService.decryptBookmark = jest.fn().mockResolvedValue({
166
- url: resultOk('url'),
380
+ url: resultOk('https://example.com/file.pdf'),
167
381
  customPassword: resultOk(undefined),
168
- nodeName: resultError('node name cannot be decrypted'),
382
+ nodeName: resultError('Node name decryption failed'),
169
383
  });
170
384
 
171
385
  const result = await Array.fromAsync(sharingAccess.iterateBookmarks());
@@ -174,15 +388,49 @@ describe('SharingAccess', () => {
174
388
  resultError({
175
389
  uid: 'tokenId',
176
390
  creationTime: new Date('2025-01-01'),
177
- url: resultOk('url'),
391
+ url: resultOk('https://example.com/file.pdf'),
178
392
  customPassword: resultOk(undefined),
179
393
  node: {
180
- name: resultError('node name cannot be decrypted'),
394
+ name: resultError('Node name decryption failed'),
395
+ type: NodeType.File,
396
+ mediaType: 'image/jpeg',
397
+ },
398
+ }),
399
+ ]);
400
+ });
401
+
402
+ it('should return degraded bookmark when all decryption fails', async () => {
403
+ cryptoService.decryptBookmark = jest.fn().mockResolvedValue({
404
+ url: resultError('URL decryption failed'),
405
+ customPassword: resultError('Password decryption failed'),
406
+ nodeName: resultError('Node name decryption failed'),
407
+ });
408
+
409
+ const result = await Array.fromAsync(sharingAccess.iterateBookmarks());
410
+
411
+ expect(result).toEqual([
412
+ resultError({
413
+ uid: 'tokenId',
414
+ creationTime: new Date('2025-01-01'),
415
+ url: resultError('URL decryption failed'),
416
+ customPassword: resultError('Password decryption failed'),
417
+ node: {
418
+ name: resultError('Node name decryption failed'),
181
419
  type: NodeType.File,
182
- mediaType: 'mediaType',
420
+ mediaType: 'image/jpeg',
183
421
  },
184
422
  }),
185
423
  ]);
186
424
  });
187
425
  });
426
+
427
+ describe('deleteBookmark', () => {
428
+ it('should delete bookmark using tokenId', async () => {
429
+ const bookmarkUid = 'tokenId123';
430
+
431
+ await sharingAccess.deleteBookmark(bookmarkUid);
432
+
433
+ expect(apiService.deleteBookmark).toHaveBeenCalledWith(bookmarkUid);
434
+ });
435
+ });
188
436
  });
@@ -124,6 +124,9 @@ export class SharingAccess {
124
124
  }
125
125
 
126
126
  await this.apiService.removeMember(memberUid);
127
+ if (await this.cache.hasSharedWithMeNodeUidsLoaded()) {
128
+ await this.cache.removeSharedWithMeNodeUid(nodeUid);
129
+ }
127
130
  }
128
131
 
129
132
  async *iterateInvitations(signal?: AbortSignal): AsyncGenerator<ProtonInvitationWithNode> {
@@ -138,6 +141,9 @@ export class SharingAccess {
138
141
  const encryptedInvitation = await this.apiService.getInvitation(invitationUid);
139
142
  const { base64SessionKeySignature } = await this.cryptoService.acceptInvitation(encryptedInvitation);
140
143
  await this.apiService.acceptInvitation(invitationUid, base64SessionKeySignature);
144
+ if (await this.cache.hasSharedWithMeNodeUidsLoaded()) {
145
+ await this.cache.addSharedWithMeNodeUid(encryptedInvitation.node.uid);
146
+ }
141
147
  }
142
148
 
143
149
  async rejectInvitation(invitationUid: string): Promise<void> {