@protontech/drive-sdk 0.2.0 → 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 (75) 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/interface/sharing.d.ts +3 -1
  11. package/dist/internal/apiService/apiService.js +11 -3
  12. package/dist/internal/apiService/apiService.js.map +1 -1
  13. package/dist/internal/apiService/errorCodes.d.ts +1 -0
  14. package/dist/internal/apiService/errors.js +1 -0
  15. package/dist/internal/apiService/errors.js.map +1 -1
  16. package/dist/internal/nodes/apiService.js +3 -0
  17. package/dist/internal/nodes/apiService.js.map +1 -1
  18. package/dist/internal/nodes/apiService.test.js +18 -0
  19. package/dist/internal/nodes/apiService.test.js.map +1 -1
  20. package/dist/internal/nodes/cache.js +3 -1
  21. package/dist/internal/nodes/cache.js.map +1 -1
  22. package/dist/internal/nodes/cache.test.js +25 -0
  23. package/dist/internal/nodes/cache.test.js.map +1 -1
  24. package/dist/internal/nodes/cryptoService.js +44 -20
  25. package/dist/internal/nodes/cryptoService.js.map +1 -1
  26. package/dist/internal/nodes/nodesAccess.js +2 -2
  27. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  28. package/dist/internal/nodes/nodesManagement.js +0 -2
  29. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  30. package/dist/internal/sharing/cryptoService.d.ts +1 -0
  31. package/dist/internal/sharing/cryptoService.js +14 -5
  32. package/dist/internal/sharing/cryptoService.js.map +1 -1
  33. package/dist/internal/sharing/interface.d.ts +0 -4
  34. package/dist/internal/sharing/sharingAccess.d.ts +1 -0
  35. package/dist/internal/sharing/sharingAccess.js +10 -3
  36. package/dist/internal/sharing/sharingAccess.js.map +1 -1
  37. package/dist/internal/sharing/sharingAccess.test.js +9 -3
  38. package/dist/internal/sharing/sharingAccess.test.js.map +1 -1
  39. package/dist/internal/sharing/sharingManagement.js +27 -16
  40. package/dist/internal/sharing/sharingManagement.js.map +1 -1
  41. package/dist/internal/sharing/sharingManagement.test.js +29 -13
  42. package/dist/internal/sharing/sharingManagement.test.js.map +1 -1
  43. package/dist/internal/upload/manager.js +1 -3
  44. package/dist/internal/upload/manager.js.map +1 -1
  45. package/dist/internal/upload/manager.test.js +2 -2
  46. package/dist/internal/upload/manager.test.js.map +1 -1
  47. package/dist/protonDriveClient.d.ts +8 -8
  48. package/dist/protonDriveClient.js +14 -14
  49. package/dist/protonDriveClient.js.map +1 -1
  50. package/package.json +1 -1
  51. package/src/diagnostic/interface.ts +27 -29
  52. package/src/diagnostic/sdkDiagnostic.ts +58 -30
  53. package/src/errors.ts +5 -5
  54. package/src/interface/author.ts +1 -1
  55. package/src/interface/events.ts +1 -7
  56. package/src/interface/sharing.ts +3 -1
  57. package/src/internal/apiService/apiService.ts +12 -3
  58. package/src/internal/apiService/errorCodes.ts +1 -0
  59. package/src/internal/apiService/errors.ts +1 -0
  60. package/src/internal/nodes/apiService.test.ts +28 -0
  61. package/src/internal/nodes/apiService.ts +3 -0
  62. package/src/internal/nodes/cache.test.ts +28 -0
  63. package/src/internal/nodes/cache.ts +3 -1
  64. package/src/internal/nodes/cryptoService.ts +68 -34
  65. package/src/internal/nodes/nodesAccess.ts +2 -2
  66. package/src/internal/nodes/nodesManagement.ts +0 -3
  67. package/src/internal/sharing/cryptoService.ts +19 -5
  68. package/src/internal/sharing/interface.ts +0 -4
  69. package/src/internal/sharing/sharingAccess.test.ts +10 -4
  70. package/src/internal/sharing/sharingAccess.ts +10 -2
  71. package/src/internal/sharing/sharingManagement.test.ts +54 -24
  72. package/src/internal/sharing/sharingManagement.ts +47 -16
  73. package/src/internal/upload/manager.test.ts +2 -2
  74. package/src/internal/upload/manager.ts +2 -4
  75. package/src/protonDriveClient.ts +17 -16
@@ -411,10 +411,7 @@ export class SharingCryptoService {
411
411
  };
412
412
  case PublicLinkFlags.GeneratedPasswordIncluded:
413
413
  case PublicLinkFlags.GeneratedPasswordWithCustomPassword:
414
- return {
415
- password: password.substring(0, PUBLIC_LINK_GENERATED_PASSWORD_LENGTH),
416
- customPassword: password.substring(PUBLIC_LINK_GENERATED_PASSWORD_LENGTH) || undefined,
417
- };
414
+ return splitGeneratedAndCustomPassword(password);
418
415
  default:
419
416
  throw new Error(`Unsupported public link with flags: ${encryptedPublicLink.flags}`);
420
417
  }
@@ -422,18 +419,24 @@ export class SharingCryptoService {
422
419
 
423
420
  async decryptBookmark(encryptedBookmark: EncryptedBookmark): Promise<{
424
421
  url: Result<string, Error>;
422
+ customPassword: Result<string | undefined, Error>;
425
423
  nodeName: Result<string, Error | InvalidNameError>;
426
424
  }> {
427
425
  // TODO: Signatures are not checked and not specified in the interface.
428
426
  // In the future, we will need to add authorship verification.
429
427
 
430
428
  let urlPassword: string;
429
+ let customPassword: Result<string | undefined, Error>;
431
430
  try {
432
- urlPassword = await this.decryptBookmarkUrlPassword(encryptedBookmark);
431
+ const password = await this.decryptBookmarkUrlPassword(encryptedBookmark);
432
+ const result = splitGeneratedAndCustomPassword(password);
433
+ urlPassword = result.password;
434
+ customPassword = resultOk(result.customPassword);
433
435
  } catch (originalError: unknown) {
434
436
  const error = originalError instanceof Error ? originalError : new Error(c('Error').t`Unknown error`);
435
437
  return {
436
438
  url: resultError(error),
439
+ customPassword: resultError(error),
437
440
  nodeName: resultError(error),
438
441
  };
439
442
  }
@@ -448,6 +451,7 @@ export class SharingCryptoService {
448
451
  const error = originalError instanceof Error ? originalError : new Error(c('Error').t`Unknown error`);
449
452
  return {
450
453
  url,
454
+ customPassword,
451
455
  nodeName: resultError(error),
452
456
  };
453
457
  }
@@ -456,6 +460,7 @@ export class SharingCryptoService {
456
460
 
457
461
  return {
458
462
  url,
463
+ customPassword,
459
464
  nodeName,
460
465
  };
461
466
  }
@@ -551,3 +556,12 @@ export class SharingCryptoService {
551
556
  }
552
557
  }
553
558
  }
559
+
560
+ function splitGeneratedAndCustomPassword(concatenatedPassword: string): {
561
+ password: string;
562
+ customPassword?: string;
563
+ } {
564
+ const password = concatenatedPassword.substring(0, PUBLIC_LINK_GENERATED_PASSWORD_LENGTH);
565
+ const customPassword = concatenatedPassword.substring(PUBLIC_LINK_GENERATED_PASSWORD_LENGTH) || undefined;
566
+ return { password, customPassword };
567
+ }
@@ -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
  });
@@ -116,6 +116,7 @@ describe('SharingAccess', () => {
116
116
  it('should return decrypted bookmark', async () => {
117
117
  cryptoService.decryptBookmark = jest.fn().mockResolvedValue({
118
118
  url: resultOk('url'),
119
+ customPassword: resultOk('customPassword'),
119
120
  nodeName: resultOk('nodeName'),
120
121
  });
121
122
 
@@ -126,6 +127,7 @@ describe('SharingAccess', () => {
126
127
  uid: 'tokenId',
127
128
  creationTime: new Date('2025-01-01'),
128
129
  url: 'url',
130
+ customPassword: 'customPassword',
129
131
  node: {
130
132
  name: 'nodeName',
131
133
  type: NodeType.File,
@@ -138,6 +140,7 @@ describe('SharingAccess', () => {
138
140
  it('should return degraded bookmark if URL password cannot be decrypted', async () => {
139
141
  cryptoService.decryptBookmark = jest.fn().mockResolvedValue({
140
142
  url: resultError('url cannot be decrypted'),
143
+ customPassword: resultOk('url cannot be decrypted'),
141
144
  nodeName: resultError('url cannot be decrypted'),
142
145
  });
143
146
 
@@ -148,6 +151,7 @@ describe('SharingAccess', () => {
148
151
  uid: 'tokenId',
149
152
  creationTime: new Date('2025-01-01'),
150
153
  url: resultError('url cannot be decrypted'),
154
+ customPassword: resultOk('url cannot be decrypted'),
151
155
  node: {
152
156
  name: resultError('url cannot be decrypted'),
153
157
  type: NodeType.File,
@@ -160,6 +164,7 @@ describe('SharingAccess', () => {
160
164
  it('should return degraded bookmark if node name cannot be decrypted', async () => {
161
165
  cryptoService.decryptBookmark = jest.fn().mockResolvedValue({
162
166
  url: resultOk('url'),
167
+ customPassword: resultOk(undefined),
163
168
  nodeName: resultError('node name cannot be decrypted'),
164
169
  });
165
170
 
@@ -170,6 +175,7 @@ describe('SharingAccess', () => {
170
175
  uid: 'tokenId',
171
176
  creationTime: new Date('2025-01-01'),
172
177
  url: resultOk('url'),
178
+ customPassword: resultOk(undefined),
173
179
  node: {
174
180
  name: resultError('node name cannot be decrypted'),
175
181
  type: NodeType.File,
@@ -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);
@@ -140,13 +146,14 @@ export class SharingAccess {
140
146
 
141
147
  async *iterateBookmarks(signal?: AbortSignal): AsyncGenerator<MaybeBookmark> {
142
148
  for await (const bookmark of this.apiService.iterateBookmarks(signal)) {
143
- const { url, nodeName } = await this.cryptoService.decryptBookmark(bookmark);
149
+ const { url, customPassword, nodeName } = await this.cryptoService.decryptBookmark(bookmark);
144
150
 
145
- if (!url.ok || !nodeName.ok) {
151
+ if (!url.ok || !customPassword.ok || !nodeName.ok) {
146
152
  yield resultError({
147
153
  uid: bookmark.tokenId,
148
154
  creationTime: bookmark.creationTime,
149
155
  url: url,
156
+ customPassword,
150
157
  node: {
151
158
  name: nodeName,
152
159
  type: bookmark.node.type,
@@ -158,6 +165,7 @@ export class SharingAccess {
158
165
  uid: bookmark.tokenId,
159
166
  creationTime: bookmark.creationTime,
160
167
  url: url.value,
168
+ customPassword: customPassword.value,
161
169
  node: {
162
170
  name: nodeName.value,
163
171
  type: bookmark.node.type,
@@ -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,
@@ -6,6 +6,7 @@ import {
6
6
  MaybeMissingNode,
7
7
  NodeResult,
8
8
  Revision,
9
+ RevisionOrUid,
9
10
  ShareNodeSettings,
10
11
  UnshareNodeSettings,
11
12
  ProtonInvitationOrUid,
@@ -311,7 +312,7 @@ export class ProtonDriveClient {
311
312
  * @param nodeUid - Node entity or its UID string.
312
313
  * @returns The updated node entity.
313
314
  * @throws {@link ValidationError} If the name is empty, too long, or contains a slash.
314
- * @throws {@link Error} If another node with the same name already exists.
315
+ * @throws {@link ValidationError} If another node with the same name already exists.
315
316
  */
316
317
  async renameNode(nodeUid: NodeOrUid, newName: string): Promise<MaybeNode> {
317
318
  this.logger.info(`Renaming node ${getUid(nodeUid)}`);
@@ -342,7 +343,7 @@ export class ProtonDriveClient {
342
343
  newParentNodeUid: NodeOrUid,
343
344
  signal?: AbortSignal,
344
345
  ): AsyncGenerator<NodeResult> {
345
- this.logger.info(`Moving ${nodeUids.length} nodes to ${newParentNodeUid}`);
346
+ this.logger.info(`Moving ${nodeUids.length} nodes to ${getUid(newParentNodeUid)}`);
346
347
  yield* this.nodes.management.moveNodes(getUids(nodeUids), getUid(newParentNodeUid), signal);
347
348
  }
348
349
 
@@ -453,9 +454,9 @@ export class ProtonDriveClient {
453
454
  *
454
455
  * @param revisionUid - UID of the revision to restore.
455
456
  */
456
- async restoreRevision(revisionUid: string): Promise<void> {
457
- this.logger.info(`Restoring revision ${revisionUid}`);
458
- await this.nodes.revisions.restoreRevision(revisionUid);
457
+ async restoreRevision(revisionUid: RevisionOrUid): Promise<void> {
458
+ this.logger.info(`Restoring revision ${getUid(revisionUid)}`);
459
+ await this.nodes.revisions.restoreRevision(getUid(revisionUid));
459
460
  }
460
461
 
461
462
  /**
@@ -463,9 +464,9 @@ export class ProtonDriveClient {
463
464
  *
464
465
  * @param revisionUid - UID of the revision to delete.
465
466
  */
466
- async deleteRevision(revisionUid: string): Promise<void> {
467
- this.logger.info(`Deleting revision ${revisionUid}`);
468
- await this.nodes.revisions.deleteRevision(revisionUid);
467
+ async deleteRevision(revisionUid: RevisionOrUid): Promise<void> {
468
+ this.logger.info(`Deleting revision ${getUid(revisionUid)}`);
469
+ await this.nodes.revisions.deleteRevision(getUid(revisionUid));
469
470
  }
470
471
 
471
472
  /**
@@ -527,21 +528,21 @@ export class ProtonDriveClient {
527
528
  /**
528
529
  * Accept the invitation to the shared node.
529
530
  *
530
- * @param invitationId - Invitation entity or its UID string.
531
+ * @param invitationUid - Invitation entity or its UID string.
531
532
  */
532
- async acceptInvitation(invitationId: string): Promise<void> {
533
- this.logger.info(`Accepting invitation ${invitationId}`);
534
- await this.sharing.access.acceptInvitation(invitationId);
533
+ async acceptInvitation(invitationUid: ProtonInvitationOrUid): Promise<void> {
534
+ this.logger.info(`Accepting invitation ${getUid(invitationUid)}`);
535
+ await this.sharing.access.acceptInvitation(getUid(invitationUid));
535
536
  }
536
537
 
537
538
  /**
538
539
  * Reject the invitation to the shared node.
539
540
  *
540
- * @param invitationId - Invitation entity or its UID string.
541
+ * @param invitationOrUid - Invitation entity or its UID string.
541
542
  */
542
- async rejectInvitation(invitationId: string): Promise<void> {
543
- this.logger.info(`Rejecting invitation ${invitationId}`);
544
- await this.sharing.access.rejectInvitation(invitationId);
543
+ async rejectInvitation(invitationUid: ProtonInvitationOrUid): Promise<void> {
544
+ this.logger.info(`Rejecting invitation ${getUid(invitationUid)}`);
545
+ await this.sharing.access.rejectInvitation(getUid(invitationUid));
545
546
  }
546
547
 
547
548
  /**