@protontech/drive-sdk 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/diagnostic/interface.d.ts +26 -29
  2. package/dist/diagnostic/sdkDiagnostic.js +50 -24
  3. package/dist/diagnostic/sdkDiagnostic.js.map +1 -1
  4. package/dist/errors.d.ts +3 -3
  5. package/dist/errors.js +7 -7
  6. package/dist/errors.js.map +1 -1
  7. package/dist/interface/author.d.ts +1 -1
  8. package/dist/interface/events.d.ts +1 -1
  9. package/dist/interface/events.js.map +1 -1
  10. package/dist/internal/apiService/apiService.js +11 -3
  11. package/dist/internal/apiService/apiService.js.map +1 -1
  12. package/dist/internal/nodes/apiService.js +3 -0
  13. package/dist/internal/nodes/apiService.js.map +1 -1
  14. package/dist/internal/nodes/apiService.test.js +18 -0
  15. package/dist/internal/nodes/apiService.test.js.map +1 -1
  16. package/dist/internal/nodes/cryptoService.js +44 -20
  17. package/dist/internal/nodes/cryptoService.js.map +1 -1
  18. package/dist/internal/nodes/nodesAccess.js +2 -2
  19. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  20. package/dist/internal/nodes/nodesManagement.js +0 -2
  21. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  22. package/dist/internal/sharing/interface.d.ts +0 -4
  23. package/dist/internal/sharing/sharingAccess.d.ts +1 -0
  24. package/dist/internal/sharing/sharingAccess.js +6 -1
  25. package/dist/internal/sharing/sharingAccess.js.map +1 -1
  26. package/dist/internal/sharing/sharingAccess.test.js +3 -3
  27. package/dist/internal/sharing/sharingAccess.test.js.map +1 -1
  28. package/dist/internal/sharing/sharingManagement.js +27 -16
  29. package/dist/internal/sharing/sharingManagement.js.map +1 -1
  30. package/dist/internal/sharing/sharingManagement.test.js +29 -13
  31. package/dist/internal/sharing/sharingManagement.test.js.map +1 -1
  32. package/dist/internal/upload/manager.js +1 -3
  33. package/dist/internal/upload/manager.js.map +1 -1
  34. package/dist/internal/upload/manager.test.js +2 -2
  35. package/dist/internal/upload/manager.test.js.map +1 -1
  36. package/dist/protonDriveClient.d.ts +8 -8
  37. package/dist/protonDriveClient.js +14 -14
  38. package/dist/protonDriveClient.js.map +1 -1
  39. package/package.json +1 -1
  40. package/src/diagnostic/interface.ts +27 -29
  41. package/src/diagnostic/sdkDiagnostic.ts +58 -30
  42. package/src/errors.ts +5 -5
  43. package/src/interface/author.ts +1 -1
  44. package/src/interface/events.ts +1 -7
  45. package/src/internal/apiService/apiService.ts +12 -3
  46. package/src/internal/nodes/apiService.test.ts +28 -0
  47. package/src/internal/nodes/apiService.ts +3 -0
  48. package/src/internal/nodes/cryptoService.ts +68 -34
  49. package/src/internal/nodes/nodesAccess.ts +2 -2
  50. package/src/internal/nodes/nodesManagement.ts +0 -3
  51. package/src/internal/sharing/interface.ts +0 -4
  52. package/src/internal/sharing/sharingAccess.test.ts +4 -4
  53. package/src/internal/sharing/sharingAccess.ts +6 -0
  54. package/src/internal/sharing/sharingManagement.test.ts +54 -24
  55. package/src/internal/sharing/sharingManagement.ts +47 -16
  56. package/src/internal/upload/manager.test.ts +2 -2
  57. package/src/internal/upload/manager.ts +2 -4
  58. package/src/protonDriveClient.ts +17 -16
@@ -61,6 +61,8 @@ export class NodesCryptoService {
61
61
  node: EncryptedNode,
62
62
  parentKey: PrivateKey,
63
63
  ): Promise<{ node: DecryptedUnparsedNode; keys?: DecryptedNodeKeys }> {
64
+ const start = Date.now();
65
+
64
66
  const commonNodeMetadata = {
65
67
  ...node,
66
68
  encryptedCrypto: undefined,
@@ -89,16 +91,17 @@ export class NodesCryptoService {
89
91
  : nodeParentKeys;
90
92
  }
91
93
 
92
- const { name, author: nameAuthor } = await this.decryptName(node, parentKey, nameVerificationKeys);
93
-
94
- let membership;
95
- if (node.membership) {
96
- membership = await this.decryptMembership(node);
97
- }
94
+ // Start promises early, but await them only when required to do
95
+ // as much work as possible in parallel.
96
+ const [membershipPromise, namePromise, keyPromise] = [
97
+ node.membership ? this.decryptMembership(node) : undefined,
98
+ this.decryptName(node, parentKey, nameVerificationKeys),
99
+ this.decryptKey(node, parentKey, keyVerificationKeys),
100
+ ];
98
101
 
99
102
  let passphrase, key, passphraseSessionKey, keyAuthor;
100
103
  try {
101
- const keyResult = await this.decryptKey(node, parentKey, keyVerificationKeys);
104
+ const keyResult = await keyPromise;
102
105
  passphrase = keyResult.passphrase;
103
106
  key = keyResult.key;
104
107
  passphraseSessionKey = keyResult.passphraseSessionKey;
@@ -107,12 +110,17 @@ export class NodesCryptoService {
107
110
  void this.reportDecryptionError(node, 'nodeKey', error);
108
111
  const message = getErrorMessage(error);
109
112
  const errorMessage = c('Error').t`Failed to decrypt node key: ${message}`;
113
+ const { name, author: nameAuthor } = await namePromise;
114
+ const membership = await membershipPromise;
110
115
  return {
111
116
  node: {
112
117
  ...commonNodeMetadata,
113
118
  name,
114
119
  keyAuthor: resultError({
115
- claimedAuthor: node.encryptedCrypto.signatureEmail,
120
+ claimedAuthor: getClaimedAuthor(
121
+ node.encryptedCrypto.signatureEmail,
122
+ keyVerificationKeys.length === 0,
123
+ ),
116
124
  error: errorMessage,
117
125
  }),
118
126
  nameAuthor,
@@ -131,8 +139,23 @@ export class NodesCryptoService {
131
139
  let folder;
132
140
  let folderExtendedAttributesAuthor;
133
141
  if ('folder' in node.encryptedCrypto) {
142
+ const folderExtendedAttributesVerificationKeys = node.encryptedCrypto.signatureEmail
143
+ ? signatureEmailKeys
144
+ : [key];
145
+
146
+ const [hashKeyPromise, folderExtendedAttributesPromise] = [
147
+ this.decryptHashKey(node, key, signatureEmailKeys),
148
+ this.decryptExtendedAttributes(
149
+ node,
150
+ node.encryptedCrypto.folder.armoredExtendedAttributes,
151
+ key,
152
+ folderExtendedAttributesVerificationKeys,
153
+ node.encryptedCrypto.signatureEmail,
154
+ ),
155
+ ];
156
+
134
157
  try {
135
- const hashKeyResult = await this.decryptHashKey(node, key, signatureEmailKeys);
158
+ const hashKeyResult = await hashKeyPromise;
136
159
  hashKey = hashKeyResult.hashKey;
137
160
  hashKeyAuthor = hashKeyResult.author;
138
161
  } catch (error: unknown) {
@@ -141,16 +164,7 @@ export class NodesCryptoService {
141
164
  }
142
165
 
143
166
  try {
144
- const folderExtendedAttributesVerificationKeys = node.encryptedCrypto.signatureEmail
145
- ? signatureEmailKeys
146
- : [key];
147
- const extendedAttributesResult = await this.decryptExtendedAttributes(
148
- node,
149
- node.encryptedCrypto.folder.armoredExtendedAttributes,
150
- key,
151
- folderExtendedAttributesVerificationKeys,
152
- node.encryptedCrypto.signatureEmail,
153
- );
167
+ const extendedAttributesResult = await folderExtendedAttributesPromise;
154
168
  folder = {
155
169
  extendedAttributes: extendedAttributesResult.extendedAttributes,
156
170
  };
@@ -165,10 +179,20 @@ export class NodesCryptoService {
165
179
  let contentKeyPacketSessionKey;
166
180
  let contentKeyPacketAuthor;
167
181
  if ('file' in node.encryptedCrypto) {
182
+ const [activeRevisionPromise, contentKeyPacketSessionKeyPromise] = [
183
+ this.decryptRevision(node.uid, node.encryptedCrypto.activeRevision, key),
184
+ this.driveCrypto.decryptAndVerifySessionKey(
185
+ node.encryptedCrypto.file.base64ContentKeyPacket,
186
+ node.encryptedCrypto.file.armoredContentKeyPacketSignature,
187
+ key,
188
+ // Content key packet is signed with the node key, but
189
+ // in the past some clients signed with the address key.
190
+ [key, ...keyVerificationKeys],
191
+ ),
192
+ ];
193
+
168
194
  try {
169
- activeRevision = resultOk(
170
- await this.decryptRevision(node.uid, node.encryptedCrypto.activeRevision, key),
171
- );
195
+ activeRevision = resultOk(await activeRevisionPromise);
172
196
  } catch (error: unknown) {
173
197
  void this.reportDecryptionError(node, 'nodeExtendedAttributes', error);
174
198
  const message = getErrorMessage(error);
@@ -177,18 +201,10 @@ export class NodesCryptoService {
177
201
  }
178
202
 
179
203
  try {
180
- const keySessionKeyResult = await this.driveCrypto.decryptAndVerifySessionKey(
181
- node.encryptedCrypto.file.base64ContentKeyPacket,
182
- node.encryptedCrypto.file.armoredContentKeyPacketSignature,
183
- key,
184
- // Content key packet is signed with the node key, but
185
- // in the past some clients signed with the address key.
186
- [key, ...keyVerificationKeys],
187
- );
188
-
204
+ const keySessionKeyResult = await contentKeyPacketSessionKeyPromise;
189
205
  contentKeyPacketSessionKey = keySessionKeyResult.sessionKey;
190
206
  contentKeyPacketAuthor =
191
- keySessionKeyResult.verified &&
207
+ keySessionKeyResult.verified !== undefined &&
192
208
  (await this.handleClaimedAuthor(
193
209
  node,
194
210
  'nodeContentKey',
@@ -230,6 +246,13 @@ export class NodesCryptoService {
230
246
  finalKeyAuthor = keyAuthor;
231
247
  }
232
248
 
249
+ const { name, author: nameAuthor } = await namePromise;
250
+ const membership = await membershipPromise;
251
+
252
+ const end = Date.now();
253
+ const duration = end - start;
254
+ this.logger.debug(`Node ${node.uid} decrypted in ${duration}ms`);
255
+
233
256
  return {
234
257
  node: {
235
258
  ...commonNodeMetadata,
@@ -319,7 +342,7 @@ export class NodesCryptoService {
319
342
  return {
320
343
  name: resultError(new Error(errorMessage)),
321
344
  author: resultError({
322
- claimedAuthor: nameSignatureEmail,
345
+ claimedAuthor: getClaimedAuthor(nameSignatureEmail, verificationKeys.length === 0),
323
346
  error: errorMessage,
324
347
  }),
325
348
  };
@@ -635,6 +658,7 @@ export class NodesCryptoService {
635
658
  if (this.reportedVerificationErrors.has(node.uid)) {
636
659
  return;
637
660
  }
661
+ this.reportedVerificationErrors.add(node.uid);
638
662
 
639
663
  const fromBefore2024 = node.creationTime < new Date('2024-01-01');
640
664
 
@@ -661,7 +685,6 @@ export class NodesCryptoService {
661
685
  error: verificationErrors?.map((e) => e.message).join(', '),
662
686
  uid: node.uid,
663
687
  });
664
- this.reportedVerificationErrors.add(node.uid);
665
688
  }
666
689
 
667
690
  private async reportDecryptionError(node: EncryptedNode, field: MetricsDecryptionErrorField, error: unknown) {
@@ -716,3 +739,14 @@ function handleClaimedAuthor(
716
739
  error: getVerificationMessage(verified, verificationErrors, signatureType, notAvailableVerificationKeys),
717
740
  });
718
741
  }
742
+
743
+ function getClaimedAuthor(
744
+ claimedAuthor?: string,
745
+ notAvailableVerificationKeys = false,
746
+ ): string | AnonymousUser | undefined {
747
+ if (!claimedAuthor && notAvailableVerificationKeys) {
748
+ return null as AnonymousUser;
749
+ }
750
+
751
+ return claimedAuthor;
752
+ }
@@ -24,7 +24,7 @@ const BATCH_LOADING_SIZE = 30;
24
24
  // It is a trade-off between performance and memory usage.
25
25
  // Higher number means more memory usage, but faster decryption.
26
26
  // Lower number means less memory usage, but slower decryption.
27
- const DECRYPTION_CONCURRENCY = 15;
27
+ const DECRYPTION_CONCURRENCY = 30;
28
28
 
29
29
  /**
30
30
  * Provides access to node metadata.
@@ -234,7 +234,7 @@ export class NodesAccess {
234
234
 
235
235
  if (errors.length > 0) {
236
236
  this.logger.error(`Failed to decrypt ${errors.length} nodes`, errors);
237
- throw new ProtonDriveError(c('Error').t`Failed to decrypt some nodes`, { cause: errors });
237
+ throw new DecryptionError(c('Error').t`Failed to decrypt some nodes`, { cause: errors });
238
238
  }
239
239
 
240
240
  const missingNodeUids = nodeUids.filter((nodeUid) => !returnedNodeUids.includes(nodeUid));
@@ -189,11 +189,8 @@ export class NodesManagement {
189
189
  }
190
190
 
191
191
  async *deleteNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
192
- const deletedNodeUids = [];
193
-
194
192
  for await (const result of this.apiService.deleteNodes(nodeUids, signal)) {
195
193
  if (result.ok) {
196
- deletedNodeUids.push(result.uid);
197
194
  await this.nodesAccess.notifyNodeDeleted(result.uid);
198
195
  }
199
196
  yield result;
@@ -145,10 +145,6 @@ export interface SharesService {
145
145
  getMyFilesIDs(): Promise<{ volumeId: string }>;
146
146
  loadEncryptedShare(shareId: string): Promise<EncryptedShare>;
147
147
  getMyFilesShareMemberEmailKey(): Promise<{
148
- addressId: string;
149
- addressKey: PrivateKey;
150
- }>;
151
- getContextShareMemberEmailKey(shareId: string): Promise<{
152
148
  email: string;
153
149
  addressId: string;
154
150
  addressKey: PrivateKey;
@@ -3,7 +3,7 @@ import { SharingAPIService } from './apiService';
3
3
  import { SharingCache } from './cache';
4
4
  import { SharingCryptoService } from './cryptoService';
5
5
  import { SharesService, NodesService } from './interface';
6
- import { SharingAccess } from './sharingAccess';
6
+ import { SharingAccess, BATCH_LOADING_SIZE } from './sharingAccess';
7
7
 
8
8
  describe('SharingAccess', () => {
9
9
  let apiService: SharingAPIService;
@@ -14,7 +14,7 @@ describe('SharingAccess', () => {
14
14
 
15
15
  let sharingAccess: SharingAccess;
16
16
 
17
- const nodeUids = Array.from({ length: 15 }, (_, i) => `nodeUid${i}`);
17
+ const nodeUids = Array.from({ length: BATCH_LOADING_SIZE + 5 }, (_, i) => `nodeUid${i}`);
18
18
  const nodes = nodeUids.map((nodeUid) => ({ nodeUid }));
19
19
  const nodeUidsIterator = async function* () {
20
20
  for (const nodeUid of nodeUids) {
@@ -84,7 +84,7 @@ describe('SharingAccess', () => {
84
84
 
85
85
  expect(result).toEqual(nodes);
86
86
  expect(apiService.iterateSharedNodeUids).toHaveBeenCalledWith('volumeId', undefined);
87
- expect(nodesService.iterateNodes).toHaveBeenCalledTimes(2); // 15 / 10 per batch
87
+ expect(nodesService.iterateNodes).toHaveBeenCalledTimes(2); // Mocked is a bit more over one batch
88
88
  expect(cache.setSharedByMeNodeUids).toHaveBeenCalledWith(nodeUids);
89
89
  });
90
90
  });
@@ -107,7 +107,7 @@ describe('SharingAccess', () => {
107
107
 
108
108
  expect(result).toEqual(nodes);
109
109
  expect(apiService.iterateSharedWithMeNodeUids).toHaveBeenCalledWith(undefined);
110
- expect(nodesService.iterateNodes).toHaveBeenCalledTimes(2); // 15 / 10 per batch
110
+ expect(nodesService.iterateNodes).toHaveBeenCalledTimes(2); // Mocked is a bit more over one batch
111
111
  expect(cache.setSharedWithMeNodeUids).toHaveBeenCalledWith(nodeUids);
112
112
  });
113
113
  });
@@ -9,6 +9,10 @@ import { SharingCache } from './cache';
9
9
  import { SharingCryptoService } from './cryptoService';
10
10
  import { SharesService, NodesService } from './interface';
11
11
 
12
+ // This is the number of nodes that are loaded in parallel.
13
+ // It is a trade-off between initial wait time and overhead of API calls.
14
+ export const BATCH_LOADING_SIZE = 30;
15
+
12
16
  /**
13
17
  * Provides high-level actions for access shared nodes.
14
18
  *
@@ -66,6 +70,7 @@ export class SharingAccess {
66
70
  ): AsyncGenerator<DecryptedNode> {
67
71
  const batchLoading = new BatchLoading<string, DecryptedNode>({
68
72
  iterateItems: (nodeUids) => this.iterateNodesAndIgnoreMissingOnes(nodeUids, signal),
73
+ batchSize: BATCH_LOADING_SIZE,
69
74
  });
70
75
  for (const nodeUid of nodeUids) {
71
76
  yield* batchLoading.load(nodeUid);
@@ -81,6 +86,7 @@ export class SharingAccess {
81
86
  const loadedNodeUids = [];
82
87
  const batchLoading = new BatchLoading<string, DecryptedNode>({
83
88
  iterateItems: (nodeUids) => this.iterateNodesAndIgnoreMissingOnes(nodeUids, signal),
89
+ batchSize: BATCH_LOADING_SIZE,
84
90
  });
85
91
  for await (const nodeUid of nodeUidsIterator) {
86
92
  loadedNodeUids.push(nodeUid);
@@ -58,11 +58,9 @@ describe('SharingManagement', () => {
58
58
  };
59
59
  // @ts-expect-error No need to implement all methods for mocking
60
60
  cryptoService = {
61
- generateShareKeys: jest
62
- .fn()
63
- .mockResolvedValue({
64
- shareKey: { encrypted: 'encrypted-key', decrypted: { passphraseSessionKey: 'pass-session-key' } },
65
- }),
61
+ generateShareKeys: jest.fn().mockResolvedValue({
62
+ shareKey: { encrypted: 'encrypted-key', decrypted: { passphraseSessionKey: 'pass-session-key' } },
63
+ }),
66
64
  decryptShare: jest.fn().mockImplementation((share) => share),
67
65
  decryptInvitation: jest.fn().mockImplementation((invitation) => invitation),
68
66
  decryptExternalInvitation: jest.fn().mockImplementation((invitation) => invitation),
@@ -70,7 +68,7 @@ describe('SharingManagement', () => {
70
68
  encryptInvitation: jest.fn().mockImplementation(() => {}),
71
69
  encryptExternalInvitation: jest.fn().mockImplementation((invitation) => ({
72
70
  ...invitation,
73
- base64ExternalInvitationSignature: 'extenral-signature',
71
+ base64ExternalInvitationSignature: 'external-signature',
74
72
  })),
75
73
  decryptPublicLink: jest.fn().mockImplementation((publicLink) => publicLink),
76
74
  generatePublicLinkPassword: jest.fn().mockResolvedValue('generatedPassword'),
@@ -85,17 +83,12 @@ describe('SharingManagement', () => {
85
83
  };
86
84
  // @ts-expect-error No need to implement all methods for mocking
87
85
  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' }),
86
+ loadEncryptedShare: jest.fn().mockResolvedValue({
87
+ id: 'shareId',
88
+ addressId: 'addressId',
89
+ creatorEmail: 'address@example.com',
90
+ passphraseSessionKey: 'sharePassphraseSessionKey',
91
+ }),
99
92
  };
100
93
  // @ts-expect-error No need to implement all methods for mocking
101
94
  nodesService = {
@@ -196,13 +189,11 @@ describe('SharingManagement', () => {
196
189
  const nodeUid = 'volumeId~nodeUid';
197
190
 
198
191
  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
- }));
192
+ nodesService.getNode = jest.fn().mockImplementation((nodeUid) => ({
193
+ nodeUid,
194
+ parentUid: 'parentUid',
195
+ name: { ok: true, value: 'name' },
196
+ }));
206
197
  nodesService.notifyNodeChanged = jest.fn();
207
198
 
208
199
  const sharingInfo = await sharingManagement.shareNode(nodeUid, { users: ['email'] });
@@ -347,6 +338,24 @@ describe('SharingManagement', () => {
347
338
  expect(apiService.updateInvitation).not.toHaveBeenCalled();
348
339
  expect(apiService.inviteProtonUser).not.toHaveBeenCalled();
349
340
  });
341
+
342
+ it('should use address from the root node context share', async () => {
343
+ nodesService.getRootNodeEmailKey = jest
344
+ .fn()
345
+ .mockResolvedValue({ email: 'my-volume-email', addressKey: 'my-volume-key' });
346
+
347
+ await sharingManagement.shareNode(nodeUid, { users: ['email'] });
348
+
349
+ expect(apiService.inviteProtonUser).toHaveBeenCalledWith(
350
+ 'shareId',
351
+ {
352
+ addedByEmail: 'my-volume-email',
353
+ inviteeEmail: 'email',
354
+ role: 'viewer',
355
+ },
356
+ expect.anything(),
357
+ );
358
+ });
350
359
  });
351
360
 
352
361
  describe('external invitations', () => {
@@ -434,6 +443,27 @@ describe('SharingManagement', () => {
434
443
  expect(apiService.updateExternalInvitation).not.toHaveBeenCalled();
435
444
  expect(apiService.inviteExternalUser).not.toHaveBeenCalled();
436
445
  });
446
+
447
+ it('should use address from the root node context share', async () => {
448
+ nodesService.getRootNodeEmailKey = jest.fn().mockResolvedValue({
449
+ email: 'my-volume-email',
450
+ addressId: 'my-volume-addressId',
451
+ addressKey: 'my-volume-key',
452
+ });
453
+
454
+ await sharingManagement.shareNode(nodeUid, { users: ['email'] });
455
+
456
+ expect(apiService.inviteExternalUser).toHaveBeenCalledWith(
457
+ 'shareId',
458
+ {
459
+ inviterAddressId: 'my-volume-addressId',
460
+ inviteeEmail: 'email',
461
+ role: 'viewer',
462
+ base64Signature: 'external-signature',
463
+ },
464
+ expect.anything(),
465
+ );
466
+ });
437
467
  });
438
468
 
439
469
  describe('mix of internal and external invitations', () => {
@@ -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,
@@ -33,6 +33,12 @@ interface Share {
33
33
  passphraseSessionKey: SessionKey;
34
34
  }
35
35
 
36
+ interface ContextShareAddress {
37
+ addressId: string;
38
+ addressKey: PrivateKey;
39
+ email: string;
40
+ }
41
+
36
42
  interface EmailOptions {
37
43
  message?: string;
38
44
  nodeName?: string;
@@ -138,18 +144,22 @@ export class SharingManagement {
138
144
  throw new ValidationError(c('Error').t`Expiration date cannot be in the past`);
139
145
  }
140
146
 
147
+ let contextShareAddress: ContextShareAddress;
141
148
  let currentSharing = await this.getInternalSharingInfo(nodeUid);
142
- if (!currentSharing) {
149
+ if (currentSharing) {
150
+ contextShareAddress = await this.nodesService.getRootNodeEmailKey(nodeUid);
151
+ } else {
143
152
  const node = await this.nodesService.getNode(nodeUid);
144
- const share = await this.createShare(nodeUid);
153
+ const result = await this.createShare(nodeUid);
145
154
  currentSharing = {
146
- share,
155
+ share: result.share,
147
156
  nodeName: node.name.ok ? node.name.value : node.name.error.name,
148
157
  protonInvitations: [],
149
158
  nonProtonInvitations: [],
150
159
  members: [],
151
160
  publicLink: undefined,
152
161
  };
162
+ contextShareAddress = result.contextShareAddress;
153
163
  }
154
164
 
155
165
  const emailOptions: EmailOptions = {
@@ -187,7 +197,13 @@ export class SharingManagement {
187
197
  }
188
198
 
189
199
  this.logger.info(`Inviting user ${email} with role ${role} to node ${nodeUid}`);
190
- const invitation = await this.inviteProtonUser(currentSharing.share, email, role, emailOptions);
200
+ const invitation = await this.inviteProtonUser(
201
+ contextShareAddress,
202
+ currentSharing.share,
203
+ email,
204
+ role,
205
+ emailOptions,
206
+ );
191
207
  currentSharing.protonInvitations.push(invitation);
192
208
  }
193
209
 
@@ -225,7 +241,13 @@ export class SharingManagement {
225
241
  }
226
242
 
227
243
  this.logger.info(`Inviting external user ${email} with role ${role} to node ${nodeUid}`);
228
- const invitation = await this.inviteExternalUser(currentSharing.share, email, role, emailOptions);
244
+ const invitation = await this.inviteExternalUser(
245
+ contextShareAddress,
246
+ currentSharing.share,
247
+ email,
248
+ role,
249
+ emailOptions,
250
+ );
229
251
  currentSharing.nonProtonInvitations.push(invitation);
230
252
  }
231
253
 
@@ -241,7 +263,7 @@ export class SharingManagement {
241
263
  );
242
264
  } else {
243
265
  this.logger.info(`Sharing via public link with role ${options.role} to node ${nodeUid}`);
244
- currentSharing.publicLink = await this.shareViaLink(currentSharing.share, options);
266
+ currentSharing.publicLink = await this.shareViaLink(contextShareAddress, currentSharing.share, options);
245
267
  }
246
268
  }
247
269
 
@@ -371,7 +393,7 @@ export class SharingManagement {
371
393
  };
372
394
  }
373
395
 
374
- private async createShare(nodeUid: string): Promise<Share> {
396
+ private async createShare(nodeUid: string): Promise<{ share: Share; contextShareAddress: ContextShareAddress }> {
375
397
  const node = await this.nodesService.getNode(nodeUid);
376
398
  if (!node.parentUid) {
377
399
  throw new ValidationError(c('Error').t`Cannot share root folder`);
@@ -387,12 +409,22 @@ export class SharingManagement {
387
409
  base64NameKeyPacket: keys.base64NameKeyPacket,
388
410
  });
389
411
  await this.nodesService.notifyNodeChanged(nodeUid);
390
- return {
412
+
413
+ const share = {
391
414
  volumeId,
392
415
  shareId,
393
416
  creatorEmail: email,
394
417
  passphraseSessionKey: keys.shareKey.decrypted.passphraseSessionKey,
395
418
  };
419
+ const contextShareAddress = {
420
+ email,
421
+ addressId,
422
+ addressKey,
423
+ };
424
+ return {
425
+ share,
426
+ contextShareAddress,
427
+ };
396
428
  }
397
429
 
398
430
  private async deleteShare(shareId: string, nodeUid: string): Promise<void> {
@@ -401,12 +433,12 @@ export class SharingManagement {
401
433
  }
402
434
 
403
435
  private async inviteProtonUser(
436
+ inviter: ContextShareAddress,
404
437
  share: Share,
405
438
  inviteeEmail: string,
406
439
  role: MemberRole,
407
440
  emailOptions: EmailOptions,
408
441
  ): Promise<ProtonInvitation> {
409
- const inviter = await this.sharesService.getContextShareMemberEmailKey(share.shareId);
410
442
  const invitationCrypto = await this.cryptoService.encryptInvitation(
411
443
  share.passphraseSessionKey,
412
444
  inviter.addressKey,
@@ -461,12 +493,12 @@ export class SharingManagement {
461
493
  }
462
494
 
463
495
  private async inviteExternalUser(
496
+ inviter: ContextShareAddress,
464
497
  share: Share,
465
498
  inviteeEmail: string,
466
499
  role: MemberRole,
467
500
  emailOptions: EmailOptions,
468
501
  ): Promise<NonProtonInvitation> {
469
- const inviter = await this.sharesService.getContextShareMemberEmailKey(share.shareId);
470
502
  const invitationCrypto = await this.cryptoService.encryptExternalInvitation(
471
503
  share.passphraseSessionKey,
472
504
  inviter.addressKey,
@@ -515,21 +547,20 @@ export class SharingManagement {
515
547
  }
516
548
 
517
549
  private async shareViaLink(
550
+ inviter: ContextShareAddress,
518
551
  share: Share,
519
552
  options: SharePublicLinkSettingsObject,
520
553
  ): Promise<PublicLinkWithCreatorEmail> {
521
- const { email: creatorEmail } = await this.sharesService.getContextShareMemberEmailKey(share.shareId);
522
-
523
554
  const generatedPassword = await this.cryptoService.generatePublicLinkPassword();
524
555
  const password = options.customPassword ? `${generatedPassword}${options.customPassword}` : generatedPassword;
525
556
 
526
557
  const { crypto, srp } = await this.cryptoService.encryptPublicLink(
527
- creatorEmail,
558
+ inviter.email,
528
559
  share.passphraseSessionKey,
529
560
  password,
530
561
  );
531
562
  const publicLink = await this.apiService.createPublicLink(share.shareId, {
532
- creatorEmail,
563
+ creatorEmail: inviter.email,
533
564
  role: options.role,
534
565
  includesCustomPassword: !!options.customPassword,
535
566
  expirationTime: options.expiration ? Math.floor(options.expiration.getTime() / 1000) : undefined,
@@ -545,7 +576,7 @@ export class SharingManagement {
545
576
  customPassword: options.customPassword,
546
577
  expirationTime: options.expiration,
547
578
  numberOfInitializedDownloads: 0,
548
- creatorEmail,
579
+ creatorEmail: inviter.email,
549
580
  };
550
581
  }
551
582
 
@@ -206,7 +206,7 @@ describe('UploadManager', () => {
206
206
  await promise;
207
207
  } catch (error: any) {
208
208
  expect(error.message).toBe('Draft already exists');
209
- expect(error.ongoingUploadByOtherClient).toBe(true);
209
+ expect(error.isUnfinishedUpload).toBe(true);
210
210
  }
211
211
  expect(apiService.deleteDraft).not.toHaveBeenCalled();
212
212
  });
@@ -237,7 +237,7 @@ describe('UploadManager', () => {
237
237
  await promise;
238
238
  } catch (error: any) {
239
239
  expect(error.message).toBe('Draft already exists');
240
- expect(error.ongoingUploadByOtherClient).toBe(true);
240
+ expect(error.isUnfinishedUpload).toBe(true);
241
241
  }
242
242
  expect(apiService.deleteDraft).not.toHaveBeenCalled();
243
243
  });
@@ -1,7 +1,7 @@
1
1
  import { c } from 'ttag';
2
2
 
3
3
  import { Logger, ProtonDriveTelemetry, UploadMetadata } from '../../interface';
4
- import { ValidationError, NodeAlreadyExistsValidationError } from '../../errors';
4
+ import { ValidationError, NodeWithSameNameExistsValidationError } from '../../errors';
5
5
  import { ErrorCode } from '../apiService';
6
6
  import { generateFileExtendedAttributes } from '../nodes';
7
7
  import { UploadAPIService } from './apiService';
@@ -158,9 +158,7 @@ export class UploadManager {
158
158
  ? makeNodeUid(splitNodeUid(parentFolderUid).volumeId, typedDetails.ConflictLinkID)
159
159
  : undefined;
160
160
 
161
- // If there is existing node, return special error
162
- // that includes the available name the client can use.
163
- throw new NodeAlreadyExistsValidationError(
161
+ throw new NodeWithSameNameExistsValidationError(
164
162
  error.message,
165
163
  error.code,
166
164
  existingNodeUid,