@protontech/drive-sdk 0.0.10 → 0.0.11

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 (215) hide show
  1. package/dist/crypto/driveCrypto.d.ts +11 -0
  2. package/dist/crypto/driveCrypto.js +16 -0
  3. package/dist/crypto/driveCrypto.js.map +1 -1
  4. package/dist/crypto/interface.d.ts +2 -0
  5. package/dist/crypto/openPGPCrypto.d.ts +2 -0
  6. package/dist/crypto/openPGPCrypto.js +8 -0
  7. package/dist/crypto/openPGPCrypto.js.map +1 -1
  8. package/dist/interface/index.d.ts +3 -3
  9. package/dist/interface/index.js +2 -2
  10. package/dist/interface/index.js.map +1 -1
  11. package/dist/interface/nodes.d.ts +9 -0
  12. package/dist/interface/nodes.js.map +1 -1
  13. package/dist/interface/sharing.d.ts +22 -2
  14. package/dist/interface/telemetry.d.ts +10 -8
  15. package/dist/interface/telemetry.js +7 -7
  16. package/dist/interface/telemetry.js.map +1 -1
  17. package/dist/internal/apiService/errors.js +1 -1
  18. package/dist/internal/apiService/errors.js.map +1 -1
  19. package/dist/internal/apiService/errors.test.js +7 -0
  20. package/dist/internal/apiService/errors.test.js.map +1 -1
  21. package/dist/internal/download/interface.d.ts +2 -2
  22. package/dist/internal/download/telemetry.js +7 -5
  23. package/dist/internal/download/telemetry.js.map +1 -1
  24. package/dist/internal/download/telemetry.test.js +10 -6
  25. package/dist/internal/download/telemetry.test.js.map +1 -1
  26. package/dist/internal/nodes/cache.js +25 -1
  27. package/dist/internal/nodes/cache.js.map +1 -1
  28. package/dist/internal/nodes/cache.test.js +33 -0
  29. package/dist/internal/nodes/cache.test.js.map +1 -1
  30. package/dist/internal/nodes/cryptoService.d.ts +8 -3
  31. package/dist/internal/nodes/cryptoService.js +11 -11
  32. package/dist/internal/nodes/cryptoService.js.map +1 -1
  33. package/dist/internal/nodes/cryptoService.test.js +2 -2
  34. package/dist/internal/nodes/cryptoService.test.js.map +1 -1
  35. package/dist/internal/nodes/index.d.ts +1 -1
  36. package/dist/internal/nodes/interface.d.ts +2 -2
  37. package/dist/internal/nodes/nodesManagement.js +1 -1
  38. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  39. package/dist/internal/nodes/nodesManagement.test.js +1 -1
  40. package/dist/internal/nodes/nodesManagement.test.js.map +1 -1
  41. package/dist/internal/shares/cryptoService.js +4 -4
  42. package/dist/internal/shares/cryptoService.js.map +1 -1
  43. package/dist/internal/shares/cryptoService.test.js +2 -2
  44. package/dist/internal/shares/cryptoService.test.js.map +1 -1
  45. package/dist/internal/shares/manager.d.ts +2 -2
  46. package/dist/internal/shares/manager.js +2 -2
  47. package/dist/internal/shares/manager.js.map +1 -1
  48. package/dist/internal/sharing/apiService.d.ts +2 -2
  49. package/dist/internal/sharing/apiService.js +1 -1
  50. package/dist/internal/sharing/apiService.js.map +1 -1
  51. package/dist/internal/sharing/cryptoService.d.ts +12 -3
  52. package/dist/internal/sharing/cryptoService.js +110 -1
  53. package/dist/internal/sharing/cryptoService.js.map +1 -1
  54. package/dist/internal/sharing/cryptoService.test.js +132 -0
  55. package/dist/internal/sharing/cryptoService.test.js.map +1 -0
  56. package/dist/internal/sharing/index.js +1 -1
  57. package/dist/internal/sharing/index.js.map +1 -1
  58. package/dist/internal/sharing/interface.d.ts +4 -0
  59. package/dist/internal/sharing/sharingAccess.d.ts +3 -3
  60. package/dist/internal/sharing/sharingAccess.js +29 -4
  61. package/dist/internal/sharing/sharingAccess.js.map +1 -1
  62. package/dist/internal/sharing/sharingAccess.test.js +65 -0
  63. package/dist/internal/sharing/sharingAccess.test.js.map +1 -1
  64. package/dist/internal/sharing/sharingManagement.js +2 -2
  65. package/dist/internal/sharing/sharingManagement.js.map +1 -1
  66. package/dist/internal/sharing/sharingManagement.test.js +3 -3
  67. package/dist/internal/sharing/sharingManagement.test.js.map +1 -1
  68. package/dist/internal/upload/interface.d.ts +2 -2
  69. package/dist/internal/upload/telemetry.js +7 -5
  70. package/dist/internal/upload/telemetry.js.map +1 -1
  71. package/dist/internal/upload/telemetry.test.js +10 -6
  72. package/dist/internal/upload/telemetry.test.js.map +1 -1
  73. package/dist/protonDriveClient.d.ts +16 -1
  74. package/dist/protonDriveClient.js +23 -2
  75. package/dist/protonDriveClient.js.map +1 -1
  76. package/dist/transformers.d.ts +1 -1
  77. package/dist/transformers.js +16 -2
  78. package/dist/transformers.js.map +1 -1
  79. package/package.json +1 -1
  80. package/src/crypto/driveCrypto.ts +30 -0
  81. package/src/crypto/interface.ts +6 -0
  82. package/src/crypto/openPGPCrypto.ts +13 -0
  83. package/src/interface/index.ts +3 -3
  84. package/src/interface/nodes.ts +9 -0
  85. package/src/interface/sharing.ts +24 -2
  86. package/src/interface/telemetry.ts +8 -7
  87. package/src/internal/apiService/errors.test.ts +8 -0
  88. package/src/internal/apiService/errors.ts +1 -1
  89. package/src/internal/download/interface.ts +2 -2
  90. package/src/internal/download/telemetry.test.ts +18 -14
  91. package/src/internal/download/telemetry.ts +8 -5
  92. package/src/internal/nodes/cache.test.ts +41 -2
  93. package/src/internal/nodes/cache.ts +31 -3
  94. package/src/internal/nodes/cryptoService.test.ts +3 -3
  95. package/src/internal/nodes/cryptoService.ts +14 -13
  96. package/src/internal/nodes/index.ts +1 -1
  97. package/src/internal/nodes/interface.ts +2 -2
  98. package/src/internal/nodes/nodesManagement.test.ts +6 -6
  99. package/src/internal/nodes/nodesManagement.ts +2 -2
  100. package/src/internal/shares/cryptoService.test.ts +2 -2
  101. package/src/internal/shares/cryptoService.ts +7 -7
  102. package/src/internal/shares/manager.ts +4 -4
  103. package/src/internal/sharing/apiService.ts +10 -10
  104. package/src/internal/sharing/cryptoService.test.ts +148 -0
  105. package/src/internal/sharing/cryptoService.ts +137 -3
  106. package/src/internal/sharing/index.ts +1 -1
  107. package/src/internal/sharing/interface.ts +4 -0
  108. package/src/internal/sharing/sharingAccess.test.ts +74 -0
  109. package/src/internal/sharing/sharingAccess.ts +29 -5
  110. package/src/internal/sharing/sharingManagement.test.ts +3 -3
  111. package/src/internal/sharing/sharingManagement.ts +2 -2
  112. package/src/internal/upload/interface.ts +2 -2
  113. package/src/internal/upload/telemetry.test.ts +10 -6
  114. package/src/internal/upload/telemetry.ts +8 -5
  115. package/src/protonDriveClient.ts +27 -2
  116. package/src/transformers.ts +31 -5
  117. package/dist/cache/index.d.ts +0 -2
  118. package/dist/cache/index.js +0 -6
  119. package/dist/cache/index.js.map +0 -1
  120. package/dist/cache/interface.d.ts +0 -105
  121. package/dist/cache/interface.js +0 -3
  122. package/dist/cache/interface.js.map +0 -1
  123. package/dist/cache/memoryCache.d.ts +0 -18
  124. package/dist/cache/memoryCache.js +0 -78
  125. package/dist/cache/memoryCache.js.map +0 -1
  126. package/dist/cache/memoryCache.test.js +0 -121
  127. package/dist/cache/memoryCache.test.js.map +0 -1
  128. package/dist/crypto/hmac.d.ts +0 -22
  129. package/dist/crypto/hmac.js +0 -44
  130. package/dist/crypto/hmac.js.map +0 -1
  131. package/dist/crypto/utils.d.ts +0 -2
  132. package/dist/crypto/utils.js +0 -35
  133. package/dist/crypto/utils.js.map +0 -1
  134. package/dist/errors.d.ts +0 -138
  135. package/dist/errors.js +0 -163
  136. package/dist/errors.js.map +0 -1
  137. package/dist/interface/account.js +0 -3
  138. package/dist/interface/account.js.map +0 -1
  139. package/dist/interface/author.d.ts +0 -26
  140. package/dist/interface/author.js +0 -3
  141. package/dist/interface/author.js.map +0 -1
  142. package/dist/interface/download.d.ts +0 -29
  143. package/dist/interface/download.js +0 -3
  144. package/dist/interface/download.js.map +0 -1
  145. package/dist/interface/httpClient.d.ts +0 -38
  146. package/dist/interface/httpClient.js +0 -3
  147. package/dist/interface/httpClient.js.map +0 -1
  148. package/dist/interface/result.d.ts +0 -9
  149. package/dist/interface/result.js +0 -11
  150. package/dist/interface/result.js.map +0 -1
  151. package/dist/interface/thumbnail.d.ts +0 -17
  152. package/dist/interface/thumbnail.js +0 -9
  153. package/dist/interface/thumbnail.js.map +0 -1
  154. package/dist/interface/upload.d.ts +0 -16
  155. package/dist/interface/upload.js +0 -3
  156. package/dist/interface/upload.js.map +0 -1
  157. package/dist/internal/apiService/errorCodes.d.ts +0 -30
  158. package/dist/internal/apiService/errorCodes.js +0 -11
  159. package/dist/internal/apiService/errorCodes.js.map +0 -1
  160. package/dist/internal/apiService/observerStream.d.ts +0 -3
  161. package/dist/internal/apiService/observerStream.js +0 -15
  162. package/dist/internal/apiService/observerStream.js.map +0 -1
  163. package/dist/internal/batchLoading.d.ts +0 -34
  164. package/dist/internal/batchLoading.js +0 -68
  165. package/dist/internal/batchLoading.js.map +0 -1
  166. package/dist/internal/batchLoading.test.d.ts +0 -1
  167. package/dist/internal/batchLoading.test.js +0 -50
  168. package/dist/internal/batchLoading.test.js.map +0 -1
  169. package/dist/internal/download/controller.d.ts +0 -8
  170. package/dist/internal/download/controller.js +0 -22
  171. package/dist/internal/download/controller.js.map +0 -1
  172. package/dist/internal/download/queue.d.ts +0 -5
  173. package/dist/internal/download/queue.js +0 -31
  174. package/dist/internal/download/queue.js.map +0 -1
  175. package/dist/internal/errors.js +0 -28
  176. package/dist/internal/errors.js.map +0 -1
  177. package/dist/internal/errors.test.js +0 -22
  178. package/dist/internal/errors.test.js.map +0 -1
  179. package/dist/internal/events/interface.d.ts +0 -47
  180. package/dist/internal/events/interface.js +0 -12
  181. package/dist/internal/events/interface.js.map +0 -1
  182. package/dist/internal/nodes/mediaTypes.d.ts +0 -2
  183. package/dist/internal/nodes/mediaTypes.js +0 -13
  184. package/dist/internal/nodes/mediaTypes.js.map +0 -1
  185. package/dist/internal/nodes/validations.d.ts +0 -4
  186. package/dist/internal/nodes/validations.js +0 -21
  187. package/dist/internal/nodes/validations.js.map +0 -1
  188. package/dist/internal/uids.d.ts +0 -38
  189. package/dist/internal/uids.js +0 -85
  190. package/dist/internal/uids.js.map +0 -1
  191. package/dist/internal/upload/chunkStreamReader.d.ts +0 -13
  192. package/dist/internal/upload/chunkStreamReader.js +0 -46
  193. package/dist/internal/upload/chunkStreamReader.js.map +0 -1
  194. package/dist/internal/upload/chunkStreamReader.test.d.ts +0 -1
  195. package/dist/internal/upload/chunkStreamReader.test.js +0 -75
  196. package/dist/internal/upload/chunkStreamReader.test.js.map +0 -1
  197. package/dist/internal/upload/controller.d.ts +0 -8
  198. package/dist/internal/upload/controller.js +0 -25
  199. package/dist/internal/upload/controller.js.map +0 -1
  200. package/dist/internal/upload/digests.d.ts +0 -8
  201. package/dist/internal/upload/digests.js +0 -22
  202. package/dist/internal/upload/digests.js.map +0 -1
  203. package/dist/internal/upload/queue.d.ts +0 -5
  204. package/dist/internal/upload/queue.js +0 -32
  205. package/dist/internal/upload/queue.js.map +0 -1
  206. package/dist/internal/utils.d.ts +0 -1
  207. package/dist/internal/utils.js +0 -13
  208. package/dist/internal/utils.js.map +0 -1
  209. package/dist/internal/wait.d.ts +0 -3
  210. package/dist/internal/wait.js +0 -28
  211. package/dist/internal/wait.js.map +0 -1
  212. package/dist/internal/wait.test.d.ts +0 -1
  213. package/dist/internal/wait.test.js +0 -21
  214. package/dist/internal/wait.test.js.map +0 -1
  215. /package/dist/{cache/memoryCache.test.d.ts → internal/sharing/cryptoService.test.d.ts} +0 -0
@@ -2,10 +2,11 @@ import bcrypt from 'bcryptjs';
2
2
  import { c } from 'ttag';
3
3
 
4
4
  import { DriveCrypto, PrivateKey, SessionKey, SRPVerifier, uint8ArrayToBase64String, VERIFICATION_STATUS } from '../../crypto';
5
- import { ProtonDriveAccount, ProtonInvitation, ProtonInvitationWithNode, NonProtonInvitation, Author, Result, Member, UnverifiedAuthorError, resultError, resultOk } from "../../interface";
5
+ import { ProtonDriveAccount, ProtonInvitation, ProtonInvitationWithNode, NonProtonInvitation, Author, Result, Member, UnverifiedAuthorError, resultError, resultOk, InvalidNameError, ProtonDriveTelemetry, MetricVolumeType } from "../../interface";
6
+ import { validateNodeName } from '../nodes/validations';
6
7
  import { getErrorMessage, getVerificationMessage } from "../errors";
7
8
  import { EncryptedShare } from "../shares";
8
- import { EncryptedInvitation, EncryptedInvitationWithNode, EncryptedExternalInvitation, EncryptedMember, EncryptedPublicLink, PublicLinkWithCreatorEmail } from "./interface";
9
+ import { EncryptedInvitation, EncryptedInvitationWithNode, EncryptedExternalInvitation, EncryptedMember, EncryptedPublicLink, PublicLinkWithCreatorEmail, EncryptedBookmark, SharesService } from "./interface";
9
10
 
10
11
  // Version 2 of bcrypt with 2**10 rounds.
11
12
  // https://en.wikipedia.org/wiki/Bcrypt#Description
@@ -31,11 +32,15 @@ enum PublicLinkFlags {
31
32
  */
32
33
  export class SharingCryptoService {
33
34
  constructor(
35
+ private telemetry: ProtonDriveTelemetry,
34
36
  private driveCrypto: DriveCrypto,
35
37
  private account: ProtonDriveAccount,
38
+ private sharesService: SharesService,
36
39
  ) {
40
+ this.telemetry = telemetry;
37
41
  this.driveCrypto = driveCrypto;
38
42
  this.account = account;
43
+ this.sharesService = sharesService;
39
44
  }
40
45
 
41
46
  /**
@@ -346,7 +351,7 @@ export class SharingCryptoService {
346
351
  }
347
352
 
348
353
  private async decryptShareUrlPassword(
349
- encryptedPublicLink: EncryptedPublicLink,
354
+ encryptedPublicLink: Pick<EncryptedPublicLink, 'armoredUrlPassword' | 'flags'>,
350
355
  addressKeys: PrivateKey[],
351
356
  ): Promise<{
352
357
  password: string,
@@ -375,4 +380,133 @@ export class SharingCryptoService {
375
380
  throw new Error(`Unsupported public link with flags: ${encryptedPublicLink.flags}`);
376
381
  }
377
382
  }
383
+
384
+ async decryptBookmark(encryptedBookmark: EncryptedBookmark): Promise<{
385
+ url: Result<string, Error>,
386
+ nodeName: Result<string, Error | InvalidNameError>,
387
+ }> {
388
+ // TODO: Signatures are not checked and not specified in the interface.
389
+ // In the future, we will need to add authorship verification.
390
+
391
+ let urlPassword: string;
392
+ try {
393
+ urlPassword = await this.decryptBookmarkUrlPassword(encryptedBookmark);
394
+ } catch (originalError: unknown) {
395
+ const error = originalError instanceof Error ? originalError : new Error(c('Error').t`Unknown error`);
396
+ return {
397
+ url: resultError(error),
398
+ nodeName: resultError(error),
399
+ };
400
+ }
401
+
402
+ // TODO: API should provide the full URL.
403
+ const url = resultOk<string, Error>(`https://drive.proton.me/urls/${encryptedBookmark.tokenId}#${urlPassword}`);
404
+
405
+ let shareKey: PrivateKey;
406
+ try {
407
+ shareKey = await this.decryptBookmarkKey(encryptedBookmark, urlPassword);
408
+ } catch (originalError: unknown) {
409
+ const error = originalError instanceof Error ? originalError : new Error(c('Error').t`Unknown error`);
410
+ return {
411
+ url,
412
+ nodeName: resultError(error),
413
+ };
414
+ }
415
+
416
+ const nodeName = await this.decryptBookmarkName(encryptedBookmark, shareKey);
417
+
418
+ return {
419
+ url,
420
+ nodeName,
421
+ };
422
+ }
423
+
424
+ private async decryptBookmarkUrlPassword(encryptedBookmark: EncryptedBookmark): Promise<string> {
425
+ if (!encryptedBookmark.url.encryptedUrlPassword) {
426
+ throw new Error(c('Error').t`Bookmark password is not available`);
427
+ }
428
+
429
+ const { addressId } = await this.sharesService.getMyFilesShareMemberEmailKey();
430
+ const address = await this.account.getOwnAddress(addressId);
431
+ const addressKeys = address.keys.map(({ key }) => key);
432
+
433
+ try {
434
+ // Decrypt the password for the share URL.
435
+ const urlPassword = await this.driveCrypto.decryptShareUrlPassword(
436
+ encryptedBookmark.url.encryptedUrlPassword,
437
+ addressKeys,
438
+ );
439
+
440
+ return urlPassword;
441
+ } catch (error: unknown) {
442
+ this.telemetry.logEvent({
443
+ eventName: 'decryptionError',
444
+ volumeType: MetricVolumeType.SharedPublic,
445
+ field: 'shareUrlPassword',
446
+ error,
447
+ });
448
+
449
+ const message = getErrorMessage(error);
450
+ const errorMessage = c('Error').t`Failed to decrypt bookmark password: ${message}`;
451
+ throw new Error(errorMessage);
452
+ }
453
+ }
454
+
455
+ private async decryptBookmarkKey(encryptedBookmark: EncryptedBookmark, urlPassword: string): Promise<PrivateKey> {
456
+ try {
457
+ // Use the password to decrypt the share key.
458
+ const { key: shareKey } = await this.driveCrypto.decryptKeyWithSrpPassword(
459
+ urlPassword,
460
+ encryptedBookmark.url.base64SharePasswordSalt,
461
+ encryptedBookmark.share.armoredKey,
462
+ encryptedBookmark.share.armoredPassphrase,
463
+ );
464
+
465
+ return shareKey;
466
+ } catch (error: unknown) {
467
+ this.telemetry.logEvent({
468
+ eventName: 'decryptionError',
469
+ volumeType: MetricVolumeType.SharedPublic,
470
+ field: 'shareKey',
471
+ error,
472
+ });
473
+
474
+ const message = getErrorMessage(error);
475
+ const errorMessage = c('Error').t`Failed to decrypt bookmark key: ${message}`;
476
+ throw new Error(errorMessage);
477
+ }
478
+ }
479
+
480
+ private async decryptBookmarkName(encryptedBookmark: EncryptedBookmark, shareKey: PrivateKey): Promise<Result<string, Error | InvalidNameError>> {
481
+ try {
482
+ // Use the share key to decrypt the node name of the bookmark.
483
+ const { name } = await this.driveCrypto.decryptNodeName(
484
+ encryptedBookmark.node.encryptedName,
485
+ shareKey,
486
+ [],
487
+ );
488
+
489
+ try {
490
+ validateNodeName(name);
491
+ } catch (error: unknown) {
492
+ return resultError({
493
+ name,
494
+ error: error instanceof Error ? error.message : c('Error').t`Unknown error`,
495
+ });
496
+ }
497
+
498
+ return resultOk(name);
499
+ } catch (error: unknown) {
500
+ this.telemetry.logEvent({
501
+ eventName: 'decryptionError',
502
+ volumeType: MetricVolumeType.SharedPublic,
503
+ field: 'nodeName',
504
+ error,
505
+ });
506
+
507
+ const message = getErrorMessage(error);
508
+ const errorMessage = c('Error').t`Failed to decrypt bookmark name: ${message}`;
509
+ return resultError(new Error(errorMessage));
510
+ }
511
+ }
378
512
  }
@@ -30,7 +30,7 @@ export function initSharingModule(
30
30
  ) {
31
31
  const api = new SharingAPIService(telemetry.getLogger('sharing-api'), apiService);
32
32
  const cache = new SharingCache(driveEntitiesCache);
33
- const cryptoService = new SharingCryptoService(crypto, account);
33
+ const cryptoService = new SharingCryptoService(telemetry, crypto, account, sharesService);
34
34
  const sharingAccess = new SharingAccess(api, cache, cryptoService, sharesService, nodesService);
35
35
  const sharingEvents = new SharingEvents(telemetry.getLogger('sharing-events'), driveEvents, cache, nodesService, sharingAccess);
36
36
  const sharingManagement = new SharingManagement(telemetry.getLogger('sharing'), api, cryptoService, account, sharesService, nodesService, nodesEvents);
@@ -143,6 +143,10 @@ export interface PublicLinkWithCreatorEmail extends PublicLink {
143
143
  export interface SharesService {
144
144
  getMyFilesIDs(): Promise<{ volumeId: string }>,
145
145
  loadEncryptedShare(shareId: string): Promise<EncryptedShare>,
146
+ getMyFilesShareMemberEmailKey(): Promise<{
147
+ addressId: string,
148
+ addressKey: PrivateKey,
149
+ }>,
146
150
  getContextShareMemberEmailKey(shareId: string): Promise<{
147
151
  email: string,
148
152
  addressId: string,
@@ -1,3 +1,4 @@
1
+ import { NodeType, resultError, resultOk } from "../../interface";
1
2
  import { SharingAPIService } from "./apiService";
2
3
  import { SharingCache } from "./cache";
3
4
  import { SharingCryptoService } from "./cryptoService";
@@ -26,6 +27,16 @@ describe("SharingAccess", () => {
26
27
  apiService = {
27
28
  iterateSharedNodeUids: jest.fn().mockImplementation(() => nodeUidsIterator()),
28
29
  iterateSharedWithMeNodeUids: jest.fn().mockImplementation(() => nodeUidsIterator()),
30
+ iterateBookmarks: jest.fn().mockImplementation(async function* () {
31
+ yield {
32
+ tokenId: "tokenId",
33
+ creationTime: new Date('2025-01-01'),
34
+ node: {
35
+ type: NodeType.File,
36
+ mediaType: "mediaType",
37
+ },
38
+ }
39
+ }),
29
40
  }
30
41
  // @ts-expect-error No need to implement all methods for mocking
31
42
  cache = {
@@ -35,6 +46,7 @@ describe("SharingAccess", () => {
35
46
  // @ts-expect-error No need to implement all methods for mocking
36
47
  cryptoService = {
37
48
  decryptInvitation: jest.fn(),
49
+ decryptBookmark: jest.fn(),
38
50
  }
39
51
  // @ts-expect-error No need to implement all methods for mocking
40
52
  sharesService = {
@@ -99,4 +111,66 @@ describe("SharingAccess", () => {
99
111
  expect(cache.setSharedWithMeNodeUids).toHaveBeenCalledWith(nodeUids);
100
112
  });
101
113
  });
114
+
115
+ describe("iterateBookmarks", () => {
116
+ it("should return decrypted bookmark", async () => {
117
+ cryptoService.decryptBookmark = jest.fn().mockResolvedValue({
118
+ url: resultOk("url"),
119
+ nodeName: resultOk("nodeName"),
120
+ });
121
+
122
+ const result = await Array.fromAsync(sharingAccess.iterateBookmarks());
123
+
124
+ expect(result).toEqual([resultOk({
125
+ uid: "tokenId",
126
+ creationTime: new Date('2025-01-01'),
127
+ url: "url",
128
+ node: {
129
+ name: "nodeName",
130
+ type: NodeType.File,
131
+ mediaType: "mediaType",
132
+ },
133
+ })]);
134
+ });
135
+
136
+ it("should return degraded bookmark if URL password cannot be decrypted", async () => {
137
+ cryptoService.decryptBookmark = jest.fn().mockResolvedValue({
138
+ url: resultError("url cannot be decrypted"),
139
+ nodeName: resultError("url cannot be decrypted"),
140
+ });
141
+
142
+ const result = await Array.fromAsync(sharingAccess.iterateBookmarks());
143
+
144
+ expect(result).toEqual([resultError({
145
+ uid: "tokenId",
146
+ creationTime: new Date('2025-01-01'),
147
+ url: resultError("url cannot be decrypted"),
148
+ node: {
149
+ name: resultError("url cannot be decrypted"),
150
+ type: NodeType.File,
151
+ mediaType: "mediaType",
152
+ },
153
+ })]);
154
+ });
155
+
156
+ it("should return degraded bookmark if node name cannot be decrypted", async () => {
157
+ cryptoService.decryptBookmark = jest.fn().mockResolvedValue({
158
+ url: resultOk("url"),
159
+ nodeName: resultError("node name cannot be decrypted"),
160
+ });
161
+
162
+ const result = await Array.fromAsync(sharingAccess.iterateBookmarks());
163
+
164
+ expect(result).toEqual([resultError({
165
+ uid: "tokenId",
166
+ creationTime: new Date('2025-01-01'),
167
+ url: resultOk("url"),
168
+ node: {
169
+ name: resultError("node name cannot be decrypted"),
170
+ type: NodeType.File,
171
+ mediaType: "mediaType",
172
+ },
173
+ })]);
174
+ });
175
+ });
102
176
  });
@@ -1,6 +1,6 @@
1
1
  import { c } from 'ttag';
2
2
 
3
- import { ProtonInvitationWithNode } from "../../interface";
3
+ import { MaybeBookmark, ProtonInvitationWithNode, resultError, resultOk } from "../../interface";
4
4
  import { ValidationError } from "../../errors";
5
5
  import { DecryptedNode } from "../nodes";
6
6
  import { BatchLoading } from "../batchLoading";
@@ -120,14 +120,38 @@ export class SharingAccess {
120
120
  await this.apiService.rejectInvitation(invitationUid);
121
121
  }
122
122
 
123
- // FIXME: return decrypted bookmarks
124
- async* iterateSharedBookmarks(signal?: AbortSignal): AsyncGenerator<string> {
123
+ async* iterateBookmarks(signal?: AbortSignal): AsyncGenerator<MaybeBookmark> {
125
124
  for await (const bookmark of this.apiService.iterateBookmarks(signal)) {
126
- yield bookmark.tokenId;
125
+ const { url, nodeName } = await this.cryptoService.decryptBookmark(bookmark);
126
+
127
+ if (!url.ok || !nodeName.ok) {
128
+ yield resultError({
129
+ uid: bookmark.tokenId,
130
+ creationTime: bookmark.creationTime,
131
+ url: url,
132
+ node: {
133
+ name: nodeName,
134
+ type: bookmark.node.type,
135
+ mediaType: bookmark.node.mediaType,
136
+ }
137
+ });
138
+ } else {
139
+ yield resultOk({
140
+ uid: bookmark.tokenId,
141
+ creationTime: bookmark.creationTime,
142
+ url: url.value,
143
+ node: {
144
+ name: nodeName.value,
145
+ type: bookmark.node.type,
146
+ mediaType: bookmark.node.mediaType,
147
+ }
148
+ });
149
+ }
127
150
  }
128
151
  }
129
152
 
130
- async deleteBookmark(tokenId: string): Promise<void> {
153
+ async deleteBookmark(bookmarkUid: string): Promise<void> {
154
+ const tokenId = bookmarkUid;
131
155
  await this.apiService.deleteBookmark(tokenId);
132
156
  }
133
157
  }
@@ -533,7 +533,7 @@ describe("SharingManagement", () => {
533
533
  expect(apiService.createPublicLink).toHaveBeenCalledWith("shareId", expect.objectContaining({
534
534
  role: MemberRole.Viewer,
535
535
  includesCustomPassword: false,
536
- expirationDuration: undefined,
536
+ expirationTime: undefined,
537
537
  crypto: "publicLinkCrypto",
538
538
  srp: "publicLinkSrp",
539
539
  }));
@@ -570,7 +570,7 @@ describe("SharingManagement", () => {
570
570
  expect(apiService.createPublicLink).toHaveBeenCalledWith("shareId", expect.objectContaining({
571
571
  role: MemberRole.Viewer,
572
572
  includesCustomPassword: true,
573
- expirationDuration: 86400,
573
+ expirationTime: 1735776000,
574
574
  crypto: "publicLinkCrypto",
575
575
  srp: "publicLinkSrp",
576
576
  }));
@@ -617,7 +617,7 @@ describe("SharingManagement", () => {
617
617
  expect(apiService.updatePublicLink).toHaveBeenCalledWith("publicLinkUid", expect.objectContaining({
618
618
  role: MemberRole.Editor,
619
619
  includesCustomPassword: true,
620
- expirationDuration: 86400,
620
+ expirationTime: 1735776000,
621
621
  crypto: "publicLinkCrypto",
622
622
  srp: "publicLinkSrp",
623
623
  }));
@@ -474,7 +474,7 @@ export class SharingManagement {
474
474
  creatorEmail,
475
475
  role: options.role,
476
476
  includesCustomPassword: !!options.customPassword,
477
- expirationDuration: options.expiration ? Math.floor((options.expiration.getTime() - Date.now()) / 1000) : undefined,
477
+ expirationTime: options.expiration ? Math.floor(options.expiration.getTime() / 1000) : undefined,
478
478
  crypto,
479
479
  srp,
480
480
  });
@@ -502,7 +502,7 @@ export class SharingManagement {
502
502
  await this.apiService.updatePublicLink(publicLink.uid, {
503
503
  role: options.role,
504
504
  includesCustomPassword: !!options.customPassword,
505
- expirationDuration: options.expiration ? Math.floor((options.expiration.getTime() - Date.now()) / 1000) : undefined,
505
+ expirationTime: options.expiration ? Math.floor(options.expiration.getTime() / 1000) : undefined,
506
506
  crypto,
507
507
  srp,
508
508
  });
@@ -1,6 +1,6 @@
1
1
  import { PrivateKey, SessionKey } from "../../crypto";
2
2
 
3
- import { MetricContext, ThumbnailType, Result, Revision } from "../../interface";
3
+ import { MetricVolumeType, ThumbnailType, Result, Revision } from "../../interface";
4
4
  import { DecryptedNode } from "../nodes";
5
5
 
6
6
  export type NodeRevisionDraft = {
@@ -124,5 +124,5 @@ export interface NodesServiceNode {
124
124
  * Interface describing the dependencies to the shares module.
125
125
  */
126
126
  export interface SharesService {
127
- getVolumeMetricContext(volumeId: string): Promise<MetricContext>,
127
+ getVolumeMetricContext(volumeId: string): Promise<MetricVolumeType>,
128
128
  }
@@ -31,26 +31,30 @@ describe('UploadTelemetry', () => {
31
31
  });
32
32
 
33
33
  it('should log failure during init (excludes uploaded size)', async () => {
34
- await uploadTelemetry.uploadInitFailed(parentNodeUid, new Error('Failed'), 1000);
34
+ const error = new Error('Failed');
35
+ await uploadTelemetry.uploadInitFailed(parentNodeUid, error, 1000);
35
36
 
36
37
  expect(mockTelemetry.logEvent).toHaveBeenCalledWith({
37
38
  eventName: "upload",
38
- context: "own_volume",
39
+ volumeType: "own_volume",
39
40
  uploadedSize: 0,
40
41
  expectedSize: 1000,
41
42
  error: "unknown",
43
+ originalError: error,
42
44
  });
43
45
  });
44
46
 
45
47
  it('should log failure upload', async () => {
46
- await uploadTelemetry.uploadFailed(revisionUid, new Error('Failed'), 500, 1000);
48
+ const error = new Error('Failed');
49
+ await uploadTelemetry.uploadFailed(revisionUid, error, 500, 1000);
47
50
 
48
51
  expect(mockTelemetry.logEvent).toHaveBeenCalledWith({
49
52
  eventName: "upload",
50
- context: "own_volume",
53
+ volumeType: "own_volume",
51
54
  uploadedSize: 500,
52
55
  expectedSize: 1000,
53
56
  error: "unknown",
57
+ originalError: error,
54
58
  });
55
59
  });
56
60
 
@@ -59,7 +63,7 @@ describe('UploadTelemetry', () => {
59
63
 
60
64
  expect(mockTelemetry.logEvent).toHaveBeenCalledWith({
61
65
  eventName: "upload",
62
- context: "own_volume",
66
+ volumeType: "own_volume",
63
67
  uploadedSize: 1000,
64
68
  expectedSize: 1000,
65
69
  });
@@ -109,7 +113,7 @@ describe('UploadTelemetry', () => {
109
113
  it('should detect "5xx" error for APIHTTPError with 5xx status code', async () => {
110
114
  const error = new APIHTTPError('Server error', 500);
111
115
  await uploadTelemetry.uploadFailed(revisionUid, error, 500, 1000);
112
- verifyErrorCategory('5xx');
116
+ verifyErrorCategory('server_error');
113
117
  });
114
118
 
115
119
  it('should detect "server_error" for TimeoutError', async () => {
@@ -40,6 +40,7 @@ export class UploadTelemetry {
40
40
  uploadedSize: 0,
41
41
  expectedSize,
42
42
  error: errorCategory,
43
+ originalError: error,
43
44
  });
44
45
  }
45
46
 
@@ -57,6 +58,7 @@ export class UploadTelemetry {
57
58
  uploadedSize,
58
59
  expectedSize,
59
60
  error: errorCategory,
61
+ originalError: error,
60
62
  });
61
63
  }
62
64
 
@@ -72,17 +74,18 @@ export class UploadTelemetry {
72
74
  uploadedSize: number,
73
75
  expectedSize: number,
74
76
  error?: MetricsUploadErrorType,
77
+ originalError?: unknown,
75
78
  }) {
76
- let context;
79
+ let volumeType;
77
80
  try {
78
- context = await this.sharesService.getVolumeMetricContext(volumeId);
81
+ volumeType = await this.sharesService.getVolumeMetricContext(volumeId);
79
82
  } catch (error: unknown) {
80
- this.logger.error('Failed to get metric context', error);
83
+ this.logger.error('Failed to get metric volume type', error);
81
84
  }
82
85
 
83
86
  this.telemetry.logEvent({
84
87
  eventName: 'upload',
85
- context,
88
+ volumeType,
86
89
  ...options,
87
90
  });
88
91
  }
@@ -103,7 +106,7 @@ function getErrorCategory(error: unknown): MetricsUploadErrorType | undefined {
103
106
  return '4xx';
104
107
  }
105
108
  if (error.statusCode >= 500) {
106
- return '5xx';
109
+ return 'server_error';
107
110
  }
108
111
  }
109
112
  if (error instanceof Error) {
@@ -11,6 +11,8 @@ import {
11
11
  ProtonInvitationOrUid,
12
12
  NonProtonInvitationOrUid,
13
13
  ProtonInvitationWithNode,
14
+ MaybeBookmark,
15
+ BookmarkOrUid,
14
16
  ShareResult,
15
17
  Device,
16
18
  DeviceType,
@@ -410,7 +412,7 @@ export class ProtonDriveClient {
410
412
  * @throws {@link Error} If another node with the same name already exists.
411
413
  */
412
414
  async renameNode(nodeUid: NodeOrUid, newName: string): Promise<MaybeNode> {
413
- this.logger.info(`Renaming node ${nodeUid} to ${newName}`);
415
+ this.logger.info(`Renaming node ${getUid(nodeUid)}`);
414
416
  return convertInternalNodePromise(this.nodes.management.renameNode(getUid(nodeUid), newName));
415
417
  }
416
418
 
@@ -511,7 +513,7 @@ export class ProtonDriveClient {
511
513
  * @throws {@link Error} If another node with the same name already exists.
512
514
  */
513
515
  async createFolder(parentNodeUid: NodeOrUid, name: string, modificationTime?: Date): Promise<MaybeNode> {
514
- this.logger.info(`Creating folder ${name} in ${getUid(parentNodeUid)}`);
516
+ this.logger.info(`Creating folder in ${getUid(parentNodeUid)}`);
515
517
  return convertInternalNodePromise(this.nodes.management.createFolder(getUid(parentNodeUid), name, modificationTime));
516
518
  }
517
519
 
@@ -645,6 +647,29 @@ export class ProtonDriveClient {
645
647
  await this.sharing.access.rejectInvitation(invitationId);
646
648
  }
647
649
 
650
+ /**
651
+ * Iterates the shared bookmarks.
652
+ *
653
+ * The output is not sorted and the order of the bookmarks is not guaranteed.
654
+ *
655
+ * @param signal - Signal to abort the operation.
656
+ * @returns An async generator of the shared bookmarks.
657
+ */
658
+ async* iterateBookmarks(signal?: AbortSignal): AsyncGenerator<MaybeBookmark> {
659
+ this.logger.info('Iterating shared bookmarks');
660
+ yield* this.sharing.access.iterateBookmarks(signal);
661
+ }
662
+
663
+ /**
664
+ * Remove the shared bookmark.
665
+ *
666
+ * @param bookmarkOrUid - Bookmark entity or its UID string.
667
+ */
668
+ async removeBookmark(bookmarkOrUid: BookmarkOrUid): Promise<void> {
669
+ this.logger.info(`Removing bookmark ${getUid(bookmarkOrUid)}`);
670
+ await this.sharing.access.deleteBookmark(getUid(bookmarkOrUid));
671
+ }
672
+
648
673
  /**
649
674
  * Get sharing info of the node.
650
675
  *
@@ -1,5 +1,15 @@
1
- import { MaybeNode as PublicMaybeNode, MaybeMissingNode as PublicMaybeMissingNode, NodeEntity as PublicNodeEntity, DegradedNode as PublicDegradedNode, Result, resultOk, resultError, MissingNode } from './interface';
2
- import { DecryptedNode as InternalNode } from './internal/nodes';
1
+ import {
2
+ MaybeNode as PublicMaybeNode,
3
+ MaybeMissingNode as PublicMaybeMissingNode,
4
+ NodeEntity as PublicNodeEntity,
5
+ DegradedNode as PublicDegradedNode,
6
+ Revision as PublicRevision,
7
+ Result,
8
+ resultOk,
9
+ resultError,
10
+ MissingNode,
11
+ } from './interface';
12
+ import { DecryptedNode as InternalNode, DecryptedRevision as InternalRevision } from './internal/nodes';
3
13
 
4
14
  type InternalPartialNode = Pick<
5
15
  InternalNode,
@@ -17,7 +27,8 @@ type InternalPartialNode = Pick<
17
27
  'activeRevision' |
18
28
  'folder' |
19
29
  'totalStorageSize' |
20
- 'errors'
30
+ 'errors' |
31
+ 'shareId'
21
32
  >;
22
33
 
23
34
  type NodeUid = string | { uid: string } | Result<{ uid: string }, { uid: string }>;
@@ -76,6 +87,7 @@ export function convertInternalNode(node: InternalPartialNode): PublicMaybeNode
76
87
  trashTime: node.trashTime,
77
88
  totalStorageSize: node.totalStorageSize,
78
89
  folder: node.folder,
90
+ deprecatedShareId: node.shareId,
79
91
  };
80
92
 
81
93
  const name = node.name;
@@ -85,7 +97,7 @@ export function convertInternalNode(node: InternalPartialNode): PublicMaybeNode
85
97
  return resultError({
86
98
  ...baseNodeMetadata,
87
99
  name,
88
- activeRevision,
100
+ activeRevision: activeRevision?.ok ? resultOk(convertInternalRevision(activeRevision.value)) : activeRevision,
89
101
  errors: node.errors,
90
102
  } as PublicDegradedNode);
91
103
  }
@@ -93,6 +105,20 @@ export function convertInternalNode(node: InternalPartialNode): PublicMaybeNode
93
105
  return resultOk({
94
106
  ...baseNodeMetadata,
95
107
  name: name.value,
96
- activeRevision: activeRevision?.value,
108
+ activeRevision: activeRevision?.ok ? convertInternalRevision(activeRevision.value) : undefined,
97
109
  } as PublicNodeEntity);
98
110
  }
111
+
112
+ function convertInternalRevision(revision: InternalRevision): PublicRevision {
113
+ return {
114
+ uid: revision.uid,
115
+ state: revision.state,
116
+ creationTime: revision.creationTime,
117
+ contentAuthor: revision.contentAuthor,
118
+ storageSize: revision.storageSize,
119
+ claimedSize: revision.claimedSize,
120
+ claimedModificationTime: revision.claimedModificationTime,
121
+ claimedDigests: revision.claimedDigests,
122
+ claimedAdditionalMetadata: revision.claimedAdditionalMetadata,
123
+ }
124
+ }
@@ -1,2 +0,0 @@
1
- export type { ProtonDriveCache, EntityResult } from './interface';
2
- export { MemoryCache } from './memoryCache';
@@ -1,6 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MemoryCache = void 0;
4
- var memoryCache_1 = require("./memoryCache");
5
- Object.defineProperty(exports, "MemoryCache", { enumerable: true, get: function () { return memoryCache_1.MemoryCache; } });
6
- //# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cache/index.ts"],"names":[],"mappings":";;;AACA,6CAA4C;AAAnC,0GAAA,WAAW,OAAA"}