@protontech/drive-sdk 0.6.2 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/dist/interface/index.d.ts +1 -0
  2. package/dist/interface/index.js.map +1 -1
  3. package/dist/interface/nodes.d.ts +14 -10
  4. package/dist/interface/nodes.js +5 -8
  5. package/dist/interface/nodes.js.map +1 -1
  6. package/dist/interface/photos.d.ts +62 -0
  7. package/dist/interface/photos.js +3 -0
  8. package/dist/interface/photos.js.map +1 -0
  9. package/dist/internal/apiService/apiService.d.ts +2 -2
  10. package/dist/internal/apiService/apiService.js.map +1 -1
  11. package/dist/internal/apiService/driveTypes.d.ts +1294 -517
  12. package/dist/internal/apiService/errors.js +4 -3
  13. package/dist/internal/apiService/errors.js.map +1 -1
  14. package/dist/internal/download/cryptoService.js +8 -6
  15. package/dist/internal/download/cryptoService.js.map +1 -1
  16. package/dist/internal/download/fileDownloader.d.ts +2 -1
  17. package/dist/internal/download/fileDownloader.js +6 -3
  18. package/dist/internal/download/fileDownloader.js.map +1 -1
  19. package/dist/internal/download/index.d.ts +1 -1
  20. package/dist/internal/download/index.js +3 -3
  21. package/dist/internal/download/index.js.map +1 -1
  22. package/dist/internal/errors.d.ts +1 -0
  23. package/dist/internal/errors.js +4 -0
  24. package/dist/internal/errors.js.map +1 -1
  25. package/dist/internal/nodes/apiService.d.ts +68 -16
  26. package/dist/internal/nodes/apiService.js +138 -85
  27. package/dist/internal/nodes/apiService.js.map +1 -1
  28. package/dist/internal/nodes/apiService.test.js +7 -5
  29. package/dist/internal/nodes/apiService.test.js.map +1 -1
  30. package/dist/internal/nodes/cache.d.ts +16 -8
  31. package/dist/internal/nodes/cache.js +19 -5
  32. package/dist/internal/nodes/cache.js.map +1 -1
  33. package/dist/internal/nodes/cache.test.js +1 -0
  34. package/dist/internal/nodes/cache.test.js.map +1 -1
  35. package/dist/internal/nodes/cryptoReporter.d.ts +3 -3
  36. package/dist/internal/nodes/cryptoReporter.js.map +1 -1
  37. package/dist/internal/nodes/cryptoService.d.ts +13 -22
  38. package/dist/internal/nodes/cryptoService.js +47 -16
  39. package/dist/internal/nodes/cryptoService.js.map +1 -1
  40. package/dist/internal/nodes/cryptoService.test.js +262 -17
  41. package/dist/internal/nodes/cryptoService.test.js.map +1 -1
  42. package/dist/internal/nodes/events.d.ts +2 -2
  43. package/dist/internal/nodes/events.js.map +1 -1
  44. package/dist/internal/nodes/index.test.js +1 -0
  45. package/dist/internal/nodes/index.test.js.map +1 -1
  46. package/dist/internal/nodes/interface.d.ts +14 -3
  47. package/dist/internal/nodes/nodesAccess.d.ts +36 -20
  48. package/dist/internal/nodes/nodesAccess.js +54 -29
  49. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  50. package/dist/internal/nodes/nodesManagement.d.ts +34 -14
  51. package/dist/internal/nodes/nodesManagement.js +44 -31
  52. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  53. package/dist/internal/nodes/nodesManagement.test.js +60 -14
  54. package/dist/internal/nodes/nodesManagement.test.js.map +1 -1
  55. package/dist/internal/nodes/nodesRevisions.d.ts +2 -2
  56. package/dist/internal/nodes/nodesRevisions.js.map +1 -1
  57. package/dist/internal/photos/albums.d.ts +2 -2
  58. package/dist/internal/photos/albums.js.map +1 -1
  59. package/dist/internal/photos/index.d.ts +19 -3
  60. package/dist/internal/photos/index.js +38 -8
  61. package/dist/internal/photos/index.js.map +1 -1
  62. package/dist/internal/photos/interface.d.ts +18 -9
  63. package/dist/internal/photos/nodes.d.ts +57 -0
  64. package/dist/internal/photos/nodes.js +165 -0
  65. package/dist/internal/photos/nodes.js.map +1 -0
  66. package/dist/internal/photos/timeline.d.ts +2 -2
  67. package/dist/internal/photos/timeline.js.map +1 -1
  68. package/dist/internal/photos/timeline.test.js.map +1 -1
  69. package/dist/internal/photos/upload.d.ts +2 -2
  70. package/dist/internal/photos/upload.js.map +1 -1
  71. package/dist/internal/sharingPublic/index.d.ts +6 -6
  72. package/dist/internal/sharingPublic/index.js +8 -7
  73. package/dist/internal/sharingPublic/index.js.map +1 -1
  74. package/dist/internal/sharingPublic/nodes.d.ts +16 -3
  75. package/dist/internal/sharingPublic/nodes.js +34 -2
  76. package/dist/internal/sharingPublic/nodes.js.map +1 -1
  77. package/dist/internal/sharingPublic/unauthApiService.d.ts +17 -0
  78. package/dist/internal/sharingPublic/unauthApiService.js +31 -0
  79. package/dist/internal/sharingPublic/unauthApiService.js.map +1 -0
  80. package/dist/internal/sharingPublic/unauthApiService.test.d.ts +1 -0
  81. package/dist/internal/sharingPublic/unauthApiService.test.js +27 -0
  82. package/dist/internal/sharingPublic/unauthApiService.test.js.map +1 -0
  83. package/dist/internal/upload/apiService.d.ts +4 -3
  84. package/dist/internal/upload/apiService.js.map +1 -1
  85. package/dist/internal/upload/cryptoService.d.ts +8 -3
  86. package/dist/internal/upload/cryptoService.js +45 -9
  87. package/dist/internal/upload/cryptoService.js.map +1 -1
  88. package/dist/internal/upload/fileUploader.test.js +1 -1
  89. package/dist/internal/upload/fileUploader.test.js.map +1 -1
  90. package/dist/internal/upload/interface.d.ts +25 -13
  91. package/dist/internal/upload/manager.js +7 -4
  92. package/dist/internal/upload/manager.js.map +1 -1
  93. package/dist/internal/upload/manager.test.js +5 -4
  94. package/dist/internal/upload/manager.test.js.map +1 -1
  95. package/dist/internal/upload/streamUploader.js +9 -4
  96. package/dist/internal/upload/streamUploader.js.map +1 -1
  97. package/dist/internal/upload/streamUploader.test.js +8 -5
  98. package/dist/internal/upload/streamUploader.test.js.map +1 -1
  99. package/dist/protonDriveClient.d.ts +11 -2
  100. package/dist/protonDriveClient.js +20 -4
  101. package/dist/protonDriveClient.js.map +1 -1
  102. package/dist/protonDrivePhotosClient.d.ts +8 -8
  103. package/dist/protonDrivePhotosClient.js +8 -9
  104. package/dist/protonDrivePhotosClient.js.map +1 -1
  105. package/dist/protonDrivePublicLinkClient.d.ts +9 -2
  106. package/dist/protonDrivePublicLinkClient.js +16 -5
  107. package/dist/protonDrivePublicLinkClient.js.map +1 -1
  108. package/dist/transformers.d.ts +7 -2
  109. package/dist/transformers.js +37 -0
  110. package/dist/transformers.js.map +1 -1
  111. package/package.json +1 -1
  112. package/src/interface/index.ts +1 -0
  113. package/src/interface/nodes.ts +14 -11
  114. package/src/interface/photos.ts +67 -0
  115. package/src/internal/apiService/apiService.ts +2 -2
  116. package/src/internal/apiService/driveTypes.ts +1294 -517
  117. package/src/internal/apiService/errors.ts +5 -4
  118. package/src/internal/download/cryptoService.ts +13 -6
  119. package/src/internal/download/fileDownloader.ts +4 -2
  120. package/src/internal/download/index.ts +3 -0
  121. package/src/internal/errors.ts +4 -0
  122. package/src/internal/nodes/apiService.test.ts +7 -5
  123. package/src/internal/nodes/apiService.ts +210 -124
  124. package/src/internal/nodes/cache.test.ts +1 -0
  125. package/src/internal/nodes/cache.ts +32 -13
  126. package/src/internal/nodes/cryptoReporter.ts +3 -3
  127. package/src/internal/nodes/cryptoService.test.ts +380 -18
  128. package/src/internal/nodes/cryptoService.ts +77 -36
  129. package/src/internal/nodes/events.ts +2 -2
  130. package/src/internal/nodes/index.test.ts +1 -0
  131. package/src/internal/nodes/interface.ts +17 -2
  132. package/src/internal/nodes/nodesAccess.ts +99 -54
  133. package/src/internal/nodes/nodesManagement.test.ts +69 -14
  134. package/src/internal/nodes/nodesManagement.ts +94 -48
  135. package/src/internal/nodes/nodesRevisions.ts +3 -3
  136. package/src/internal/photos/albums.ts +2 -2
  137. package/src/internal/photos/index.ts +45 -3
  138. package/src/internal/photos/interface.ts +21 -9
  139. package/src/internal/photos/nodes.ts +233 -0
  140. package/src/internal/photos/timeline.test.ts +2 -2
  141. package/src/internal/photos/timeline.ts +2 -2
  142. package/src/internal/photos/upload.ts +3 -3
  143. package/src/internal/sharingPublic/index.ts +7 -3
  144. package/src/internal/sharingPublic/nodes.ts +43 -2
  145. package/src/internal/sharingPublic/unauthApiService.test.ts +29 -0
  146. package/src/internal/sharingPublic/unauthApiService.ts +32 -0
  147. package/src/internal/upload/apiService.ts +4 -3
  148. package/src/internal/upload/cryptoService.ts +73 -12
  149. package/src/internal/upload/fileUploader.test.ts +1 -1
  150. package/src/internal/upload/interface.ts +24 -13
  151. package/src/internal/upload/manager.test.ts +5 -4
  152. package/src/internal/upload/manager.ts +7 -4
  153. package/src/internal/upload/streamUploader.test.ts +8 -5
  154. package/src/internal/upload/streamUploader.ts +10 -4
  155. package/src/protonDriveClient.ts +27 -5
  156. package/src/protonDrivePhotosClient.ts +23 -23
  157. package/src/protonDrivePublicLinkClient.ts +19 -3
  158. package/src/transformers.ts +49 -2
@@ -24,6 +24,7 @@ import {
24
24
  DecryptedNodeKeys,
25
25
  EncryptedRevision,
26
26
  DecryptedUnparsedRevision,
27
+ NodeSigningKeys,
27
28
  } from './interface';
28
29
 
29
30
  export interface NodesCryptoReporter {
@@ -33,7 +34,7 @@ export interface NodesCryptoReporter {
33
34
  signatureType: string,
34
35
  verified: VERIFICATION_STATUS,
35
36
  verificationErrors?: Error[],
36
- claimedAuthor?: string,
37
+ claimedAuthor?: string | AnonymousUser,
37
38
  notAvailableVerificationKeys?: boolean,
38
39
  ): Promise<Author>;
39
40
  reportDecryptionError(node: NodesCryptoReporterNode, field: MetricsDecryptionErrorField, error: unknown): void;
@@ -336,11 +337,19 @@ export class NodesCryptoService {
336
337
  const nameSignatureEmail = node.encryptedCrypto.nameSignatureEmail;
337
338
 
338
339
  try {
339
- const { name, verified, verificationErrors } = await this.driveCrypto.decryptNodeName(
340
- node.encryptedName,
341
- parentKey,
342
- verificationKeys,
343
- );
340
+ const {
341
+ name,
342
+ verified: verificationStatus,
343
+ verificationErrors,
344
+ } = await this.driveCrypto.decryptNodeName(node.encryptedName, parentKey, verificationKeys);
345
+
346
+ let verified = verificationStatus;
347
+ // The name was not signed until Drive web Beta 3.
348
+ // It is decided to ignore this and consider it signed.
349
+ // The problem will be gone with migration to new crypto model.
350
+ if (verificationStatus === VERIFICATION_STATUS.NOT_SIGNED && node.creationTime < new Date(2021, 0, 1)) {
351
+ verified = VERIFICATION_STATUS.SIGNED_AND_VALID;
352
+ }
344
353
 
345
354
  return {
346
355
  name: resultOk(name),
@@ -438,11 +447,19 @@ export class NodesCryptoService {
438
447
  throw new Error('Node is not a folder');
439
448
  }
440
449
 
441
- const { hashKey, verified, verificationErrors } = await this.driveCrypto.decryptNodeHashKey(
442
- node.encryptedCrypto.folder.armoredHashKey,
443
- nodeKey,
444
- addressKeys,
445
- );
450
+ const {
451
+ hashKey,
452
+ verified: verificationStatus,
453
+ verificationErrors,
454
+ } = await this.driveCrypto.decryptNodeHashKey(node.encryptedCrypto.folder.armoredHashKey, nodeKey, addressKeys);
455
+
456
+ let verified = verificationStatus;
457
+ // The hash was not signed until Drive web Beta 17.
458
+ // It is decided to ignore this and consider it signed.
459
+ // The problem will be gone with migration to new crypto model.
460
+ if (verificationStatus === VERIFICATION_STATUS.NOT_SIGNED && node.creationTime < new Date(2021, 7, 1)) {
461
+ verified = VERIFICATION_STATUS.SIGNED_AND_VALID;
462
+ }
446
463
 
447
464
  return {
448
465
  hashKey,
@@ -490,7 +507,7 @@ export class NodesCryptoService {
490
507
  encryptedExtendedAttributes: string | undefined,
491
508
  nodeKey: PrivateKey,
492
509
  addressKeys: PublicKey[],
493
- signatureEmail?: string,
510
+ signatureEmail?: string | AnonymousUser,
494
511
  ): Promise<{
495
512
  extendedAttributes?: string;
496
513
  author: Author;
@@ -522,31 +539,44 @@ export class NodesCryptoService {
522
539
 
523
540
  async createFolder(
524
541
  parentKeys: { key: PrivateKey; hashKey: Uint8Array },
525
- address: { email: string; addressKey: PrivateKey },
542
+ signingKeys: NodeSigningKeys,
526
543
  name: string,
527
544
  extendedAttributes?: string,
528
545
  ): Promise<{
529
- encryptedCrypto: EncryptedNodeFolderCrypto & {
546
+ encryptedCrypto: Omit<EncryptedNodeFolderCrypto, 'signatureEmail' | 'nameSignatureEmail'> & {
547
+ signatureEmail: string | AnonymousUser;
548
+ nameSignatureEmail: string | AnonymousUser;
530
549
  armoredNodePassphraseSignature: string;
531
- // signatureEmail and nameSignatureEmail are not optional.
532
- signatureEmail: string;
533
- nameSignatureEmail: string;
534
550
  encryptedName: string;
535
551
  hash: string;
536
552
  };
537
553
  keys: DecryptedNodeKeys;
538
554
  }> {
539
- const { email, addressKey } = address;
555
+ const email = signingKeys.type === 'userAddress' ? signingKeys.email : null;
556
+ const nameAndPassprhaseSigningKey =
557
+ signingKeys.type === 'userAddress' ? signingKeys.key : signingKeys.parentNodeKey;
558
+ if (!nameAndPassprhaseSigningKey) {
559
+ // This is a bug within the SDK.
560
+ throw new Error('Cannot create new node without a name and passphrase signing key');
561
+ }
562
+
540
563
  const [nodeKeys, { armoredNodeName }, hash] = await Promise.all([
541
- this.driveCrypto.generateKey([parentKeys.key], addressKey),
542
- this.driveCrypto.encryptNodeName(name, undefined, parentKeys.key, addressKey),
564
+ this.driveCrypto.generateKey([parentKeys.key], nameAndPassprhaseSigningKey),
565
+ this.driveCrypto.encryptNodeName(name, undefined, parentKeys.key, nameAndPassprhaseSigningKey),
543
566
  this.driveCrypto.generateLookupHash(name, parentKeys.hashKey),
544
567
  ]);
545
568
 
546
569
  const { armoredHashKey, hashKey } = await this.driveCrypto.generateHashKey(nodeKeys.decrypted.key);
547
570
 
571
+ const extendedAttributesSigningKey =
572
+ signingKeys.type === 'userAddress' ? signingKeys.key : signingKeys.nodeKey || nodeKeys.decrypted.key;
573
+
548
574
  const { armoredExtendedAttributes } = extendedAttributes
549
- ? await this.driveCrypto.encryptExtendedAttributes(extendedAttributes, nodeKeys.decrypted.key, addressKey)
575
+ ? await this.driveCrypto.encryptExtendedAttributes(
576
+ extendedAttributes,
577
+ nodeKeys.decrypted.key,
578
+ extendedAttributesSigningKey,
579
+ )
550
580
  : { armoredExtendedAttributes: undefined };
551
581
 
552
582
  return {
@@ -575,20 +605,25 @@ export class NodesCryptoService {
575
605
  async encryptNewName(
576
606
  parentKeys: { key: PrivateKey; hashKey?: Uint8Array },
577
607
  nodeNameSessionKey: SessionKey,
578
- address: { email: string; addressKey: PrivateKey },
608
+ signingKeys: NodeSigningKeys,
579
609
  newName: string,
580
610
  ): Promise<{
581
- signatureEmail: string;
611
+ signatureEmail: string | AnonymousUser;
582
612
  armoredNodeName: string;
583
613
  hash?: string;
584
614
  }> {
585
- const { email, addressKey } = address;
615
+ const email = signingKeys.type === 'userAddress' ? signingKeys.email : null;
616
+ const nameSigningKey = signingKeys.type === 'userAddress' ? signingKeys.key : signingKeys.parentNodeKey;
617
+ if (!nameSigningKey) {
618
+ // This is a bug within the SDK.
619
+ throw new Error('Cannot encrypt new node name without a name signing key');
620
+ }
586
621
 
587
622
  const { armoredNodeName } = await this.driveCrypto.encryptNodeName(
588
623
  newName,
589
624
  nodeNameSessionKey,
590
625
  parentKeys.key,
591
- addressKey,
626
+ nameSigningKey,
592
627
  );
593
628
 
594
629
  const hash = parentKeys.hashKey
@@ -602,38 +637,44 @@ export class NodesCryptoService {
602
637
  }
603
638
 
604
639
  async encryptNodeWithNewParent(
605
- node: Pick<DecryptedNode, 'name'>,
640
+ nodeName: DecryptedNode['name'],
606
641
  keys: { passphrase: string; passphraseSessionKey: SessionKey; nameSessionKey: SessionKey },
607
642
  parentKeys: { key: PrivateKey; hashKey: Uint8Array },
608
- address: { email: string; addressKey: PrivateKey },
643
+ signingKeys: NodeSigningKeys,
609
644
  ): Promise<{
610
645
  encryptedName: string;
611
646
  hash: string;
612
647
  armoredNodePassphrase: string;
613
648
  armoredNodePassphraseSignature: string;
614
- signatureEmail: string;
615
- nameSignatureEmail: string;
649
+ signatureEmail: string | AnonymousUser;
650
+ nameSignatureEmail: string | AnonymousUser;
616
651
  }> {
617
652
  if (!parentKeys.hashKey) {
618
653
  throw new ValidationError('Moving item to a non-folder is not allowed');
619
654
  }
620
- if (!node.name.ok) {
655
+ if (!nodeName.ok) {
621
656
  throw new ValidationError('Cannot move item without a valid name, please rename the item first');
622
657
  }
623
658
 
624
- const { email, addressKey } = address;
659
+ const email = signingKeys.type === 'userAddress' ? signingKeys.email : null;
660
+ const nameAndPassprhaseSigningKey = signingKeys.type === 'userAddress' ? signingKeys.key : signingKeys.nodeKey;
661
+ if (!nameAndPassprhaseSigningKey) {
662
+ // This is a bug within the SDK.
663
+ throw new Error('Cannot re-encrypt node without a name and passphrase signing key');
664
+ }
665
+
625
666
  const { armoredNodeName } = await this.driveCrypto.encryptNodeName(
626
- node.name.value,
667
+ nodeName.value,
627
668
  keys.nameSessionKey,
628
669
  parentKeys.key,
629
- addressKey,
670
+ nameAndPassprhaseSigningKey,
630
671
  );
631
- const hash = await this.driveCrypto.generateLookupHash(node.name.value, parentKeys.hashKey);
672
+ const hash = await this.driveCrypto.generateLookupHash(nodeName.value, parentKeys.hashKey);
632
673
  const { armoredPassphrase, armoredPassphraseSignature } = await this.driveCrypto.encryptPassphrase(
633
674
  keys.passphrase,
634
675
  keys.passphraseSessionKey,
635
676
  [parentKeys.key],
636
- addressKey,
677
+ nameAndPassprhaseSigningKey,
637
678
  );
638
679
 
639
680
  return {
@@ -657,7 +698,7 @@ export class NodesCryptoService {
657
698
  }
658
699
 
659
700
  function getClaimedAuthor(
660
- claimedAuthor?: string,
701
+ claimedAuthor?: string | AnonymousUser,
661
702
  notAvailableVerificationKeys = false,
662
703
  ): string | AnonymousUser | undefined {
663
704
  if (!claimedAuthor && notAvailableVerificationKeys) {
@@ -1,6 +1,6 @@
1
1
  import { Logger } from '../../interface';
2
2
  import { DriveEvent, DriveEventType } from '../events';
3
- import { NodesCache } from './cache';
3
+ import { NodesCacheBase } from './cache';
4
4
 
5
5
  /**
6
6
  * Provides internal event handling.
@@ -11,7 +11,7 @@ import { NodesCache } from './cache';
11
11
  export class NodesEventsHandler {
12
12
  constructor(
13
13
  private logger: Logger,
14
- private cache: NodesCache,
14
+ private cache: NodesCacheBase,
15
15
  ) {}
16
16
 
17
17
  async updateNodesCacheOnEvent(event: DriveEvent): Promise<void> {
@@ -26,6 +26,7 @@ function generateNode(uid: string, parentUid = 'volumeId~root', params: Partial<
26
26
  isShared: false,
27
27
  isSharedPublicly: false,
28
28
  creationTime: new Date(),
29
+ modificationTime: new Date(),
29
30
  trashTime: undefined,
30
31
  isStale: false,
31
32
  ...params,
@@ -10,6 +10,7 @@ import {
10
10
  MetricVolumeType,
11
11
  Revision,
12
12
  RevisionState,
13
+ AnonymousUser,
13
14
  } from '../../interface';
14
15
 
15
16
  export type FilterOptions = {
@@ -32,6 +33,7 @@ interface BaseNode {
32
33
  type: NodeType;
33
34
  mediaType?: string;
34
35
  creationTime: Date; // created on the server
36
+ modificationTime: Date; // modified on server
35
37
  trashTime?: Date;
36
38
  totalStorageSize?: number;
37
39
 
@@ -57,8 +59,8 @@ export interface EncryptedNode extends BaseNode {
57
59
  }
58
60
 
59
61
  export interface EncryptedNodeCrypto {
60
- signatureEmail?: string;
61
- nameSignatureEmail?: string;
62
+ signatureEmail?: string | AnonymousUser;
63
+ nameSignatureEmail?: string | AnonymousUser;
62
64
  armoredKey: string;
63
65
  armoredNodePassphrase: string;
64
66
  armoredNodePassphraseSignature?: string;
@@ -88,6 +90,19 @@ export interface EncryptedNodeFolderCrypto extends EncryptedNodeCrypto {
88
90
  // eslint-disable-next-line @typescript-eslint/no-empty-object-type
89
91
  export interface EncryptedNodeAlbumCrypto extends EncryptedNodeCrypto {}
90
92
 
93
+ export type NodeSigningKeys =
94
+ | {
95
+ type: 'userAddress';
96
+ email: string;
97
+ addressId: string;
98
+ key: PrivateKey;
99
+ }
100
+ | {
101
+ type: 'nodeKey';
102
+ nodeKey?: PrivateKey;
103
+ parentNodeKey?: PrivateKey;
104
+ };
105
+
91
106
  /**
92
107
  * Interface used only internally in the nodes module.
93
108
  *
@@ -16,8 +16,8 @@ import { asyncIteratorMap } from '../asyncIteratorMap';
16
16
  import { getErrorMessage } from '../errors';
17
17
  import { BatchLoading } from '../batchLoading';
18
18
  import { makeNodeUid, splitNodeUid } from '../uids';
19
- import { NodeAPIService } from './apiService';
20
- import { NodesCache } from './cache';
19
+ import { NodeAPIServiceBase } from './apiService';
20
+ import { NodesCacheBase } from './cache';
21
21
  import { NodesCryptoCache } from './cryptoCache';
22
22
  import { NodesCryptoService } from './cryptoService';
23
23
  import { NodesDebouncer } from './debouncer';
@@ -29,6 +29,7 @@ import {
29
29
  DecryptedNode,
30
30
  DecryptedNodeKeys,
31
31
  FilterOptions,
32
+ NodeSigningKeys,
32
33
  } from './interface';
33
34
  import { validateNodeName } from './validations';
34
35
  import { isProtonDocument, isProtonSheet } from './mediaTypes';
@@ -49,17 +50,21 @@ const DECRYPTION_CONCURRENCY = 30;
49
50
  * The node access module is responsible for fetching, decrypting and caching
50
51
  * nodes metadata.
51
52
  */
52
- export class NodesAccess {
53
- private logger: Logger;
54
- private debouncer: NodesDebouncer;
53
+ export abstract class NodesAccessBase<
54
+ TEncryptedNode extends EncryptedNode = EncryptedNode,
55
+ TDecryptedNode extends DecryptedNode = DecryptedNode,
56
+ TCryptoService extends NodesCryptoService = NodesCryptoService,
57
+ > {
58
+ protected logger: Logger;
59
+ protected debouncer: NodesDebouncer;
55
60
 
56
61
  constructor(
57
- private telemetry: ProtonDriveTelemetry,
58
- private apiService: NodeAPIService,
59
- private cache: NodesCache,
60
- private cryptoCache: NodesCryptoCache,
61
- private cryptoService: NodesCryptoService,
62
- private shareService: Pick<
62
+ protected telemetry: ProtonDriveTelemetry,
63
+ protected apiService: NodeAPIServiceBase<TEncryptedNode>,
64
+ protected cache: NodesCacheBase<TDecryptedNode>,
65
+ protected cryptoCache: NodesCryptoCache,
66
+ protected cryptoService: TCryptoService,
67
+ protected shareService: Pick<
63
68
  SharesService,
64
69
  'getRootIDs' | 'getSharePrivateKey' | 'getContextShareMemberEmailKey'
65
70
  >,
@@ -79,7 +84,7 @@ export class NodesAccess {
79
84
  return this.getNode(nodeUid);
80
85
  }
81
86
 
82
- async getNode(nodeUid: string): Promise<DecryptedNode> {
87
+ async getNode(nodeUid: string): Promise<TDecryptedNode> {
83
88
  let cachedNode;
84
89
  try {
85
90
  await this.debouncer.waitForLoadingNode(nodeUid);
@@ -100,11 +105,11 @@ export class NodesAccess {
100
105
  parentNodeUid: string,
101
106
  filterOptions?: FilterOptions,
102
107
  signal?: AbortSignal,
103
- ): AsyncGenerator<DecryptedNode> {
108
+ ): AsyncGenerator<TDecryptedNode> {
104
109
  // Ensure the parent is loaded and up-to-date.
105
110
  const parentNode = await this.getNode(parentNodeUid);
106
111
 
107
- const batchLoading = new BatchLoading<string, DecryptedNode>({
112
+ const batchLoading = new BatchLoading<string, TDecryptedNode>({
108
113
  iterateItems: (nodeUids) => this.loadNodes(nodeUids, filterOptions, signal),
109
114
  batchSize: BATCH_LOADING_SIZE,
110
115
  });
@@ -153,9 +158,9 @@ export class NodesAccess {
153
158
  }
154
159
 
155
160
  // Improvement requested: keep status of loaded trash and leverage cache.
156
- async *iterateTrashedNodes(signal?: AbortSignal): AsyncGenerator<DecryptedNode> {
161
+ async *iterateTrashedNodes(signal?: AbortSignal): AsyncGenerator<TDecryptedNode> {
157
162
  const { volumeId } = await this.shareService.getRootIDs();
158
- const batchLoading = new BatchLoading<string, DecryptedNode>({
163
+ const batchLoading = new BatchLoading<string, TDecryptedNode>({
159
164
  iterateItems: (nodeUids) => this.loadNodes(nodeUids, undefined, signal),
160
165
  batchSize: BATCH_LOADING_SIZE,
161
166
  });
@@ -176,8 +181,8 @@ export class NodesAccess {
176
181
  yield* batchLoading.loadRest();
177
182
  }
178
183
 
179
- async *iterateNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<DecryptedNode | MissingNode> {
180
- const batchLoading = new BatchLoading<string, DecryptedNode | MissingNode>({
184
+ async *iterateNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<TDecryptedNode | MissingNode> {
185
+ const batchLoading = new BatchLoading<string, TDecryptedNode | MissingNode>({
181
186
  iterateItems: (nodeUids) => this.loadNodesWithMissingReport(nodeUids, undefined, signal),
182
187
  batchSize: BATCH_LOADING_SIZE,
183
188
  });
@@ -227,7 +232,7 @@ export class NodesAccess {
227
232
  await this.cache.removeNodes([nodeUid]);
228
233
  }
229
234
 
230
- private async loadNode(nodeUid: string): Promise<{ node: DecryptedNode; keys?: DecryptedNodeKeys }> {
235
+ private async loadNode(nodeUid: string): Promise<{ node: TDecryptedNode; keys?: DecryptedNodeKeys }> {
231
236
  this.debouncer.loadingNode(nodeUid);
232
237
  try {
233
238
  const { volumeId: ownVolumeId } = await this.shareService.getRootIDs();
@@ -242,7 +247,7 @@ export class NodesAccess {
242
247
  nodeUids: string[],
243
248
  filterOptions?: FilterOptions,
244
249
  signal?: AbortSignal,
245
- ): AsyncGenerator<DecryptedNode> {
250
+ ): AsyncGenerator<TDecryptedNode> {
246
251
  for await (const result of this.loadNodesWithMissingReport(nodeUids, filterOptions, signal)) {
247
252
  if ('missingUid' in result) {
248
253
  continue;
@@ -255,7 +260,7 @@ export class NodesAccess {
255
260
  nodeUids: string[],
256
261
  filterOptions?: FilterOptions,
257
262
  signal?: AbortSignal,
258
- ): AsyncGenerator<DecryptedNode | MissingNode> {
263
+ ): AsyncGenerator<TDecryptedNode | MissingNode> {
259
264
  const returnedNodeUids: string[] = [];
260
265
  const errors = [];
261
266
 
@@ -263,13 +268,13 @@ export class NodesAccess {
263
268
 
264
269
  const apiNodesIterator = this.apiService.iterateNodes(nodeUids, ownVolumeId, filterOptions, signal);
265
270
 
266
- const debouncedNodeMapper = async (encryptedNode: EncryptedNode): Promise<EncryptedNode> => {
271
+ const debouncedNodeMapper = async (encryptedNode: TEncryptedNode): Promise<TEncryptedNode> => {
267
272
  this.debouncer.loadingNode(encryptedNode.uid);
268
273
  return encryptedNode;
269
274
  };
270
275
  const encryptedNodesIterator = asyncIteratorMap(apiNodesIterator, debouncedNodeMapper, 1);
271
276
 
272
- const decryptNodeMapper = async (encryptedNode: EncryptedNode): Promise<Result<DecryptedNode, unknown>> => {
277
+ const decryptNodeMapper = async (encryptedNode: TEncryptedNode): Promise<Result<TDecryptedNode, unknown>> => {
273
278
  returnedNodeUids.push(encryptedNode.uid);
274
279
  try {
275
280
  const { node } = await this.decryptNode(encryptedNode);
@@ -309,8 +314,8 @@ export class NodesAccess {
309
314
  }
310
315
 
311
316
  private async decryptNode(
312
- encryptedNode: EncryptedNode,
313
- ): Promise<{ node: DecryptedNode; keys?: DecryptedNodeKeys }> {
317
+ encryptedNode: TEncryptedNode,
318
+ ): Promise<{ node: TDecryptedNode; keys?: DecryptedNodeKeys }> {
314
319
  let parentKey;
315
320
  try {
316
321
  const parentKeys = await this.getParentKeys(encryptedNode);
@@ -318,38 +323,14 @@ export class NodesAccess {
318
323
  } catch (error: unknown) {
319
324
  if (error instanceof DecryptionError) {
320
325
  return {
321
- node: {
322
- ...encryptedNode,
323
- isStale: false,
324
- name: resultError(error),
325
- keyAuthor: resultError({
326
- claimedAuthor: encryptedNode.encryptedCrypto.signatureEmail,
327
- error: getErrorMessage(error),
328
- }),
329
- nameAuthor: resultError({
330
- claimedAuthor: encryptedNode.encryptedCrypto.nameSignatureEmail,
331
- error: getErrorMessage(error),
332
- }),
333
- membership: encryptedNode.membership
334
- ? {
335
- role: encryptedNode.membership.role,
336
- inviteTime: encryptedNode.membership.inviteTime,
337
- sharedBy: resultError({
338
- claimedAuthor: encryptedNode.encryptedCrypto.membership?.inviterEmail,
339
- error: getErrorMessage(error),
340
- }),
341
- }
342
- : undefined,
343
- errors: [error],
344
- treeEventScopeId: splitNodeUid(encryptedNode.uid).volumeId,
345
- },
326
+ node: this.getDegradedUndecryptableNode(encryptedNode, error),
346
327
  };
347
328
  }
348
329
  throw error;
349
330
  }
350
331
 
351
332
  const { node: unparsedNode, keys } = await this.cryptoService.decryptNode(encryptedNode, parentKey);
352
- const node = await parseNode(this.logger, unparsedNode);
333
+ const node = this.parseNode(unparsedNode);
353
334
  try {
354
335
  await this.cache.setNode(node);
355
336
  } catch (error: unknown) {
@@ -366,8 +347,45 @@ export class NodesAccess {
366
347
  return { node, keys };
367
348
  }
368
349
 
350
+ protected abstract getDegradedUndecryptableNode(
351
+ encryptedNode: TEncryptedNode,
352
+ error: DecryptionError,
353
+ ): TDecryptedNode;
354
+
355
+ protected getDegradedUndecryptableNodeBase(encryptedNode: EncryptedNode, error: DecryptionError): DecryptedNode {
356
+ return {
357
+ ...encryptedNode,
358
+ isStale: false,
359
+ name: resultError(error),
360
+ keyAuthor: resultError({
361
+ claimedAuthor: encryptedNode.encryptedCrypto.signatureEmail,
362
+ error: getErrorMessage(error),
363
+ }),
364
+ nameAuthor: resultError({
365
+ claimedAuthor: encryptedNode.encryptedCrypto.nameSignatureEmail,
366
+ error: getErrorMessage(error),
367
+ }),
368
+ membership: encryptedNode.membership
369
+ ? {
370
+ role: encryptedNode.membership.role,
371
+ inviteTime: encryptedNode.membership.inviteTime,
372
+ sharedBy: resultError({
373
+ claimedAuthor: encryptedNode.encryptedCrypto.membership?.inviterEmail,
374
+ error: getErrorMessage(error),
375
+ }),
376
+ }
377
+ : undefined,
378
+ errors: [error],
379
+ treeEventScopeId: splitNodeUid(encryptedNode.uid).volumeId,
380
+ };
381
+ }
382
+
383
+ protected abstract parseNode(
384
+ unparsedNode: Awaited<ReturnType<TCryptoService['decryptNode']>>['node'],
385
+ ): TDecryptedNode;
386
+
369
387
  async getParentKeys(
370
- node: Pick<DecryptedNode, 'uid' | 'parentUid' | 'shareId'>,
388
+ node: Pick<TDecryptedNode, 'uid' | 'parentUid' | 'shareId'>,
371
389
  ): Promise<Pick<DecryptedNodeKeys, 'key' | 'hashKey'>> {
372
390
  if (node.parentUid) {
373
391
  try {
@@ -425,6 +443,22 @@ export class NodesAccess {
425
443
  };
426
444
  }
427
445
 
446
+ async getNodeSigningKeys(
447
+ uids: { nodeUid: string; parentNodeUid?: string } | { nodeUid?: string; parentNodeUid: string },
448
+ ): Promise<NodeSigningKeys> {
449
+ const contextNodeUid = uids.nodeUid || uids.parentNodeUid;
450
+ if (!contextNodeUid) {
451
+ throw new Error('Context node UID is required for signing keys');
452
+ }
453
+ const address = await this.getRootNodeEmailKey(contextNodeUid);
454
+ return {
455
+ type: 'userAddress',
456
+ email: address.email,
457
+ addressId: address.addressId,
458
+ key: address.addressKey,
459
+ };
460
+ }
461
+
428
462
  async getRootNodeEmailKey(nodeUid: string): Promise<{
429
463
  email: string;
430
464
  addressId: string;
@@ -456,13 +490,23 @@ export class NodesAccess {
456
490
  return `https://drive.proton.me/${rootNode.shareId}/${type}/${nodeId}`;
457
491
  }
458
492
 
459
- private async getRootNode(nodeUid: string): Promise<DecryptedNode> {
493
+ private async getRootNode(nodeUid: string): Promise<TDecryptedNode> {
460
494
  const node = await this.getNode(nodeUid);
461
495
  return node.parentUid ? this.getRootNode(node.parentUid) : node;
462
496
  }
463
497
  }
464
498
 
465
- export async function parseNode(logger: Logger, unparsedNode: DecryptedUnparsedNode): Promise<DecryptedNode> {
499
+ export class NodesAccess extends NodesAccessBase {
500
+ protected getDegradedUndecryptableNode(encryptedNode: EncryptedNode, error: DecryptionError): DecryptedNode {
501
+ return this.getDegradedUndecryptableNodeBase(encryptedNode, error);
502
+ }
503
+
504
+ protected parseNode(unparsedNode: DecryptedUnparsedNode): DecryptedNode {
505
+ return parseNode(this.logger, unparsedNode);
506
+ }
507
+ }
508
+
509
+ export function parseNode(logger: Logger, unparsedNode: DecryptedUnparsedNode): DecryptedNode {
466
510
  let nodeName: Result<string, Error | InvalidNameError> = unparsedNode.name;
467
511
  if (unparsedNode.name.ok) {
468
512
  try {
@@ -509,6 +553,7 @@ export async function parseNode(logger: Logger, unparsedNode: DecryptedUnparsedN
509
553
  const extendedAttributes = unparsedNode.folder?.extendedAttributes
510
554
  ? parseFolderExtendedAttributes(logger, unparsedNode.folder.extendedAttributes)
511
555
  : undefined;
556
+
512
557
  return {
513
558
  ...unparsedNode,
514
559
  name: nodeName,