@protontech/drive-sdk 0.7.0 → 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 (95) 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/driveTypes.d.ts +1294 -517
  10. package/dist/internal/errors.d.ts +1 -0
  11. package/dist/internal/errors.js +4 -0
  12. package/dist/internal/errors.js.map +1 -1
  13. package/dist/internal/nodes/apiService.d.ts +60 -9
  14. package/dist/internal/nodes/apiService.js +125 -81
  15. package/dist/internal/nodes/apiService.js.map +1 -1
  16. package/dist/internal/nodes/apiService.test.js +2 -0
  17. package/dist/internal/nodes/apiService.test.js.map +1 -1
  18. package/dist/internal/nodes/cache.d.ts +16 -8
  19. package/dist/internal/nodes/cache.js +19 -5
  20. package/dist/internal/nodes/cache.js.map +1 -1
  21. package/dist/internal/nodes/cache.test.js +1 -0
  22. package/dist/internal/nodes/cache.test.js.map +1 -1
  23. package/dist/internal/nodes/cryptoService.d.ts +1 -1
  24. package/dist/internal/nodes/cryptoService.js +4 -4
  25. package/dist/internal/nodes/cryptoService.js.map +1 -1
  26. package/dist/internal/nodes/cryptoService.test.js +3 -3
  27. package/dist/internal/nodes/cryptoService.test.js.map +1 -1
  28. package/dist/internal/nodes/events.d.ts +2 -2
  29. package/dist/internal/nodes/events.js.map +1 -1
  30. package/dist/internal/nodes/index.test.js +1 -0
  31. package/dist/internal/nodes/index.test.js.map +1 -1
  32. package/dist/internal/nodes/interface.d.ts +1 -0
  33. package/dist/internal/nodes/nodesAccess.d.ts +29 -20
  34. package/dist/internal/nodes/nodesAccess.js +41 -29
  35. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  36. package/dist/internal/nodes/nodesManagement.d.ts +32 -12
  37. package/dist/internal/nodes/nodesManagement.js +30 -13
  38. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  39. package/dist/internal/nodes/nodesManagement.test.js +39 -4
  40. package/dist/internal/nodes/nodesManagement.test.js.map +1 -1
  41. package/dist/internal/nodes/nodesRevisions.d.ts +2 -2
  42. package/dist/internal/nodes/nodesRevisions.js.map +1 -1
  43. package/dist/internal/photos/albums.d.ts +2 -2
  44. package/dist/internal/photos/albums.js.map +1 -1
  45. package/dist/internal/photos/index.d.ts +19 -3
  46. package/dist/internal/photos/index.js +38 -8
  47. package/dist/internal/photos/index.js.map +1 -1
  48. package/dist/internal/photos/interface.d.ts +18 -9
  49. package/dist/internal/photos/nodes.d.ts +57 -0
  50. package/dist/internal/photos/nodes.js +165 -0
  51. package/dist/internal/photos/nodes.js.map +1 -0
  52. package/dist/internal/photos/timeline.d.ts +2 -2
  53. package/dist/internal/photos/timeline.js.map +1 -1
  54. package/dist/internal/photos/timeline.test.js.map +1 -1
  55. package/dist/protonDriveClient.d.ts +10 -1
  56. package/dist/protonDriveClient.js +18 -3
  57. package/dist/protonDriveClient.js.map +1 -1
  58. package/dist/protonDrivePhotosClient.d.ts +8 -8
  59. package/dist/protonDrivePhotosClient.js +8 -9
  60. package/dist/protonDrivePhotosClient.js.map +1 -1
  61. package/dist/protonDrivePublicLinkClient.d.ts +7 -1
  62. package/dist/protonDrivePublicLinkClient.js +9 -0
  63. package/dist/protonDrivePublicLinkClient.js.map +1 -1
  64. package/dist/transformers.d.ts +7 -2
  65. package/dist/transformers.js +37 -0
  66. package/dist/transformers.js.map +1 -1
  67. package/package.json +1 -1
  68. package/src/interface/index.ts +1 -0
  69. package/src/interface/nodes.ts +14 -11
  70. package/src/interface/photos.ts +67 -0
  71. package/src/internal/apiService/driveTypes.ts +1294 -517
  72. package/src/internal/errors.ts +4 -0
  73. package/src/internal/nodes/apiService.test.ts +2 -0
  74. package/src/internal/nodes/apiService.ts +187 -114
  75. package/src/internal/nodes/cache.test.ts +1 -0
  76. package/src/internal/nodes/cache.ts +32 -13
  77. package/src/internal/nodes/cryptoService.test.ts +13 -3
  78. package/src/internal/nodes/cryptoService.ts +4 -4
  79. package/src/internal/nodes/events.ts +2 -2
  80. package/src/internal/nodes/index.test.ts +1 -0
  81. package/src/internal/nodes/interface.ts +1 -0
  82. package/src/internal/nodes/nodesAccess.ts +82 -54
  83. package/src/internal/nodes/nodesManagement.test.ts +48 -4
  84. package/src/internal/nodes/nodesManagement.ts +76 -26
  85. package/src/internal/nodes/nodesRevisions.ts +3 -3
  86. package/src/internal/photos/albums.ts +2 -2
  87. package/src/internal/photos/index.ts +45 -3
  88. package/src/internal/photos/interface.ts +21 -9
  89. package/src/internal/photos/nodes.ts +233 -0
  90. package/src/internal/photos/timeline.test.ts +2 -2
  91. package/src/internal/photos/timeline.ts +2 -2
  92. package/src/protonDriveClient.ts +20 -3
  93. package/src/protonDrivePhotosClient.ts +23 -23
  94. package/src/protonDrivePublicLinkClient.ts +11 -0
  95. package/src/transformers.ts +49 -2
@@ -3,6 +3,10 @@ import { c } from 'ttag';
3
3
  import { VERIFICATION_STATUS } from '../crypto';
4
4
  import { AbortError, ConnectionError, RateLimitedError, ValidationError } from '../errors';
5
5
 
6
+ export function createErrorFromUnknown(error: unknown): Error {
7
+ return error instanceof Error ? error : new Error(getErrorMessage(error), { cause: error });
8
+ }
9
+
6
10
  export function getErrorMessage(error: unknown): string {
7
11
  return error instanceof Error ? error.message : c('Error').t`Unknown error`;
8
12
  }
@@ -65,6 +65,7 @@ function generateAPINode() {
65
65
  ParentLinkID: 'parentLinkId',
66
66
  NameHash: 'nameHash',
67
67
  CreateTime: 123456789,
68
+ ModifyTime: 1234567890,
68
69
  TrashTime: 0,
69
70
 
70
71
  Name: 'encName',
@@ -140,6 +141,7 @@ function generateNode() {
140
141
  uid: 'volumeId~linkId',
141
142
  parentUid: 'volumeId~parentLinkId',
142
143
  creationTime: new Date(123456789000),
144
+ modificationTime: new Date(1234567890000),
143
145
  trashTime: undefined,
144
146
 
145
147
  shareId: undefined,
@@ -114,18 +114,21 @@ type PostCheckAvailableHashesResponse =
114
114
  * The service is responsible for transforming local objects to API payloads
115
115
  * and vice versa. It should not contain any business logic.
116
116
  */
117
- export class NodeAPIService {
117
+ export abstract class NodeAPIServiceBase<
118
+ T extends EncryptedNode = EncryptedNode,
119
+ TMetadataResponseLink extends { Link: { LinkID: string } } = { Link: { LinkID: string } },
120
+ > {
118
121
  constructor(
119
- private logger: Logger,
120
- private apiService: DriveAPIService,
121
- private clientUid: string | undefined,
122
+ protected logger: Logger,
123
+ protected apiService: DriveAPIService,
124
+ protected clientUid: string | undefined,
122
125
  ) {
123
126
  this.logger = logger;
124
127
  this.apiService = apiService;
125
128
  this.clientUid = clientUid;
126
129
  }
127
130
 
128
- async getNode(nodeUid: string, ownVolumeId: string, signal?: AbortSignal): Promise<EncryptedNode> {
131
+ async getNode(nodeUid: string, ownVolumeId: string, signal?: AbortSignal): Promise<T> {
129
132
  const nodesGenerator = this.iterateNodes([nodeUid], ownVolumeId, undefined, signal);
130
133
  const result = await nodesGenerator.next();
131
134
  if (!result.value) {
@@ -140,7 +143,7 @@ export class NodeAPIService {
140
143
  ownVolumeId: string | undefined,
141
144
  filterOptions?: FilterOptions,
142
145
  signal?: AbortSignal,
143
- ): AsyncGenerator<EncryptedNode> {
146
+ ): AsyncGenerator<T> {
144
147
  const allNodeIds = nodeUids.map(splitNodeUid);
145
148
 
146
149
  const nodeIdsByVolumeId = new Map<string, string[]>();
@@ -184,27 +187,21 @@ export class NodeAPIService {
184
187
  }
185
188
  }
186
189
 
187
- private async *iterateNodesPerVolume(
190
+ protected async *iterateNodesPerVolume(
188
191
  volumeId: string,
189
192
  nodeIds: string[],
190
193
  isOwnVolumeId: boolean,
191
194
  filterOptions?: FilterOptions,
192
195
  signal?: AbortSignal,
193
- ): AsyncGenerator<EncryptedNode, unknown[]> {
196
+ ): AsyncGenerator<T, unknown[]> {
194
197
  const errors: unknown[] = [];
195
198
 
196
199
  for (const nodeIdsBatch of batch(nodeIds, API_NODES_BATCH_SIZE)) {
197
- const response = await this.apiService.post<PostLoadLinksMetadataRequest, PostLoadLinksMetadataResponse>(
198
- `drive/v2/volumes/${volumeId}/links`,
199
- {
200
- LinkIDs: nodeIdsBatch,
201
- },
202
- signal,
203
- );
200
+ const responseLinks = await this.fetchNodeMetadata(volumeId, nodeIdsBatch, signal);
204
201
 
205
- for (const link of response.Links) {
202
+ for (const link of responseLinks) {
206
203
  try {
207
- const encryptedNode = linkToEncryptedNode(this.logger, volumeId, link, isOwnVolumeId);
204
+ const encryptedNode = this.linkToEncryptedNode(volumeId, link, isOwnVolumeId);
208
205
  if (filterOptions?.type && encryptedNode.type !== filterOptions.type) {
209
206
  continue;
210
207
  }
@@ -219,6 +216,14 @@ export class NodeAPIService {
219
216
  return errors;
220
217
  }
221
218
 
219
+ protected abstract fetchNodeMetadata(
220
+ volumeId: string,
221
+ linkIds: string[],
222
+ signal?: AbortSignal,
223
+ ): Promise<TMetadataResponseLink[]>;
224
+
225
+ protected abstract linkToEncryptedNode(volumeId: string, link: TMetadataResponseLink, isOwnVolumeId: boolean): T;
226
+
222
227
  // Improvement requested: load next page sooner before all IDs are yielded.
223
228
  async *iterateChildrenNodeUids(
224
229
  parentNodeUid: string,
@@ -341,24 +346,29 @@ export class NodeAPIService {
341
346
  const { volumeId, nodeId } = splitNodeUid(nodeUid);
342
347
  const { nodeId: newParentNodeId } = splitNodeUid(newNode.parentUid);
343
348
 
344
- await this.apiService.put<Omit<PutMoveNodeRequest, 'SignatureAddress' | 'MIMEType'>, PutMoveNodeResponse>(
345
- `drive/v2/volumes/${volumeId}/links/${nodeId}/move`,
346
- {
347
- ParentLinkID: newParentNodeId,
348
- NodePassphrase: newNode.armoredNodePassphrase,
349
- // @ts-expect-error: API accepts NodePassphraseSignature as optional.
350
- NodePassphraseSignature: newNode.armoredNodePassphraseSignature,
351
- // @ts-expect-error: API accepts SignatureEmail as optional.
352
- SignatureEmail: newNode.signatureEmail,
353
- Name: newNode.encryptedName,
354
- // @ts-expect-error: API accepts NameSignatureEmail as optional.
355
- NameSignatureEmail: newNode.nameSignatureEmail,
356
- Hash: newNode.hash,
357
- OriginalHash: oldNode.hash,
358
- ContentHash: newNode.contentHash || null,
359
- },
360
- signal,
361
- );
349
+ try {
350
+ await this.apiService.put<Omit<PutMoveNodeRequest, 'SignatureAddress' | 'MIMEType'>, PutMoveNodeResponse>(
351
+ `drive/v2/volumes/${volumeId}/links/${nodeId}/move`,
352
+ {
353
+ ParentLinkID: newParentNodeId,
354
+ NodePassphrase: newNode.armoredNodePassphrase,
355
+ // @ts-expect-error: API accepts NodePassphraseSignature as optional.
356
+ NodePassphraseSignature: newNode.armoredNodePassphraseSignature,
357
+ // @ts-expect-error: API accepts SignatureEmail as optional.
358
+ SignatureEmail: newNode.signatureEmail,
359
+ Name: newNode.encryptedName,
360
+ // @ts-expect-error: API accepts NameSignatureEmail as optional.
361
+ NameSignatureEmail: newNode.nameSignatureEmail,
362
+ Hash: newNode.hash,
363
+ OriginalHash: oldNode.hash,
364
+ ContentHash: newNode.contentHash || null,
365
+ },
366
+ signal,
367
+ );
368
+ } catch (error: unknown) {
369
+ handleNodeWithSameNameExistsValidationError(volumeId, error);
370
+ throw error;
371
+ }
362
372
  }
363
373
 
364
374
  async copyNode(
@@ -377,23 +387,29 @@ export class NodeAPIService {
377
387
  const { volumeId, nodeId } = splitNodeUid(nodeUid);
378
388
  const { volumeId: parentVolumeId, nodeId: parentNodeId } = splitNodeUid(newNode.parentUid);
379
389
 
380
- const response = await this.apiService.post<PostCopyNodeRequest, PostCopyNodeResponse>(
381
- `drive/volumes/${volumeId}/links/${nodeId}/copy`,
382
- {
383
- TargetVolumeID: parentVolumeId,
384
- TargetParentLinkID: parentNodeId,
385
- NodePassphrase: newNode.armoredNodePassphrase,
386
- // @ts-expect-error: API accepts NodePassphraseSignature as optional.
387
- NodePassphraseSignature: newNode.armoredNodePassphraseSignature,
388
- // @ts-expect-error: API accepts SignatureEmail as optional.
389
- SignatureEmail: newNode.signatureEmail,
390
- Name: newNode.encryptedName,
391
- // @ts-expect-error: API accepts NameSignatureEmail as optional.
392
- NameSignatureEmail: newNode.nameSignatureEmail,
393
- Hash: newNode.hash,
394
- },
395
- signal,
396
- );
390
+ let response: PostCopyNodeResponse;
391
+ try {
392
+ response = await this.apiService.post<PostCopyNodeRequest, PostCopyNodeResponse>(
393
+ `drive/volumes/${volumeId}/links/${nodeId}/copy`,
394
+ {
395
+ TargetVolumeID: parentVolumeId,
396
+ TargetParentLinkID: parentNodeId,
397
+ NodePassphrase: newNode.armoredNodePassphrase,
398
+ // @ts-expect-error: API accepts NodePassphraseSignature as optional.
399
+ NodePassphraseSignature: newNode.armoredNodePassphraseSignature,
400
+ // @ts-expect-error: API accepts SignatureEmail as optional.
401
+ SignatureEmail: newNode.signatureEmail,
402
+ Name: newNode.encryptedName,
403
+ // @ts-expect-error: API accepts NameSignatureEmail as optional.
404
+ NameSignatureEmail: newNode.nameSignatureEmail,
405
+ Hash: newNode.hash,
406
+ },
407
+ signal,
408
+ );
409
+ } catch (error: unknown) {
410
+ handleNodeWithSameNameExistsValidationError(volumeId, error);
411
+ throw error;
412
+ }
397
413
 
398
414
  return makeNodeUid(volumeId, response.LinkID);
399
415
  }
@@ -491,21 +507,7 @@ export class NodeAPIService {
491
507
  },
492
508
  );
493
509
  } catch (error: unknown) {
494
- if (error instanceof ValidationError) {
495
- if (error.code === ErrorCode.ALREADY_EXISTS) {
496
- const typedDetails = error.details as
497
- | {
498
- ConflictLinkID: string;
499
- }
500
- | undefined;
501
-
502
- const existingNodeUid = typedDetails?.ConflictLinkID
503
- ? makeNodeUid(volumeId, typedDetails.ConflictLinkID)
504
- : undefined;
505
-
506
- throw new NodeWithSameNameExistsValidationError(error.message, error.code, existingNodeUid);
507
- }
508
- }
510
+ handleNodeWithSameNameExistsValidationError(volumeId, error);
509
511
  throw error;
510
512
  }
511
513
 
@@ -583,6 +585,35 @@ export class NodeAPIService {
583
585
  }
584
586
  }
585
587
 
588
+ export class NodeAPIService extends NodeAPIServiceBase {
589
+ constructor(logger: Logger, apiService: DriveAPIService, clientUid: string | undefined) {
590
+ super(logger, apiService, clientUid);
591
+ }
592
+
593
+ protected async fetchNodeMetadata(
594
+ volumeId: string,
595
+ linkIds: string[],
596
+ signal?: AbortSignal,
597
+ ): Promise<PostLoadLinksMetadataResponse['Links']> {
598
+ const response = await this.apiService.post<PostLoadLinksMetadataRequest, PostLoadLinksMetadataResponse>(
599
+ `drive/v2/volumes/${volumeId}/links`,
600
+ {
601
+ LinkIDs: linkIds,
602
+ },
603
+ signal,
604
+ );
605
+ return response.Links;
606
+ }
607
+
608
+ protected linkToEncryptedNode(
609
+ volumeId: string,
610
+ link: PostLoadLinksMetadataResponse['Links'][0],
611
+ isOwnVolumeId: boolean,
612
+ ): EncryptedNode {
613
+ return linkToEncryptedNode(this.logger, volumeId, link, isOwnVolumeId);
614
+ }
615
+ }
616
+
586
617
  type LinkResponse = {
587
618
  LinkID: string;
588
619
  Response: {
@@ -615,56 +646,36 @@ function* handleResponseErrors(
615
646
  }
616
647
  }
617
648
 
618
- function linkToEncryptedNode(
649
+ function handleNodeWithSameNameExistsValidationError(volumeId: string, error: unknown): void {
650
+ if (error instanceof ValidationError) {
651
+ if (error.code === ErrorCode.ALREADY_EXISTS) {
652
+ const typedDetails = error.details as
653
+ | {
654
+ ConflictLinkID: string;
655
+ }
656
+ | undefined;
657
+
658
+ const existingNodeUid = typedDetails?.ConflictLinkID
659
+ ? makeNodeUid(volumeId, typedDetails.ConflictLinkID)
660
+ : undefined;
661
+
662
+ throw new NodeWithSameNameExistsValidationError(error.message, error.code, existingNodeUid);
663
+ }
664
+ }
665
+ }
666
+
667
+ export function linkToEncryptedNode(
619
668
  logger: Logger,
620
669
  volumeId: string,
621
- link: PostLoadLinksMetadataResponse['Links'][0],
670
+ link: Pick<PostLoadLinksMetadataResponse['Links'][0], 'Link' | 'Membership' | 'Sharing' | 'Folder' | 'File'>,
622
671
  isAdmin: boolean,
623
672
  ): EncryptedNode {
624
- const membershipRole = permissionsToMemberRole(logger, link.Membership?.Permissions);
625
-
626
- const baseNodeMetadata = {
627
- // Internal metadata
628
- hash: link.Link.NameHash || undefined,
629
- encryptedName: link.Link.Name,
630
-
631
- // Basic node metadata
632
- uid: makeNodeUid(volumeId, link.Link.LinkID),
633
- parentUid: link.Link.ParentLinkID ? makeNodeUid(volumeId, link.Link.ParentLinkID) : undefined,
634
- type: nodeTypeNumberToNodeType(logger, link.Link.Type),
635
- creationTime: new Date(link.Link.CreateTime * 1000),
636
- trashTime: link.Link.TrashTime ? new Date(link.Link.TrashTime * 1000) : undefined,
637
-
638
- // Sharing node metadata
639
- shareId: link.Sharing?.ShareID || undefined,
640
- isShared: !!link.Sharing,
641
- isSharedPublicly: !!link.Sharing?.ShareURLID,
642
- directRole: isAdmin ? MemberRole.Admin : membershipRole,
643
- membership: link.Membership
644
- ? {
645
- role: membershipRole,
646
- inviteTime: new Date(link.Membership.InviteTime * 1000),
647
- }
648
- : undefined,
649
- };
650
-
651
- const baseCryptoNodeMetadata = {
652
- signatureEmail: link.Link.SignatureEmail || undefined,
653
- nameSignatureEmail: link.Link.NameSignatureEmail || undefined,
654
- armoredKey: link.Link.NodeKey,
655
- armoredNodePassphrase: link.Link.NodePassphrase,
656
- armoredNodePassphraseSignature: link.Link.NodePassphraseSignature,
657
- membership: link.Membership
658
- ? {
659
- inviterEmail: link.Membership.InviterEmail,
660
- base64MemberSharePassphraseKeyPacket: link.Membership.MemberSharePassphraseKeyPacket,
661
- armoredInviterSharePassphraseKeyPacketSignature:
662
- link.Membership.InviterSharePassphraseKeyPacketSignature,
663
- armoredInviteeSharePassphraseSessionKeySignature:
664
- link.Membership.InviteeSharePassphraseSessionKeySignature,
665
- }
666
- : undefined,
667
- };
673
+ const { baseNodeMetadata, baseCryptoNodeMetadata } = linkToEncryptedNodeBaseMetadata(
674
+ logger,
675
+ volumeId,
676
+ link,
677
+ isAdmin,
678
+ );
668
679
 
669
680
  if (link.Link.Type === 1 && link.Folder) {
670
681
  return {
@@ -706,6 +717,11 @@ function linkToEncryptedNode(
706
717
  };
707
718
  }
708
719
 
720
+ // TODO: Remove this once client do not use main SDK for photos.
721
+ // At the beginning, the client used main SDK for some photo actions.
722
+ // This was a temporary solution before the Photos SDK was implemented.
723
+ // Now the client must use Photos SDK for all photo-related actions.
724
+ // Knowledge of albums in main SDK is deprecated and will be removed.
709
725
  if (link.Link.Type === 3) {
710
726
  return {
711
727
  ...baseNodeMetadata,
@@ -718,6 +734,64 @@ function linkToEncryptedNode(
718
734
  throw new Error(`Unknown node type: ${link.Link.Type}`);
719
735
  }
720
736
 
737
+ export function linkToEncryptedNodeBaseMetadata(
738
+ logger: Logger,
739
+ volumeId: string,
740
+ link: Pick<PostLoadLinksMetadataResponse['Links'][0], 'Link' | 'Membership' | 'Sharing'>,
741
+ isAdmin: boolean,
742
+ ) {
743
+ const membershipRole = permissionsToMemberRole(logger, link.Membership?.Permissions);
744
+
745
+ const baseNodeMetadata = {
746
+ // Internal metadata
747
+ hash: link.Link.NameHash || undefined,
748
+ encryptedName: link.Link.Name,
749
+
750
+ // Basic node metadata
751
+ uid: makeNodeUid(volumeId, link.Link.LinkID),
752
+ parentUid: link.Link.ParentLinkID ? makeNodeUid(volumeId, link.Link.ParentLinkID) : undefined,
753
+ type: nodeTypeNumberToNodeType(logger, link.Link.Type),
754
+ creationTime: new Date(link.Link.CreateTime * 1000),
755
+ modificationTime: new Date(link.Link.ModifyTime * 1000),
756
+ trashTime: link.Link.TrashTime ? new Date(link.Link.TrashTime * 1000) : undefined,
757
+
758
+ // Sharing node metadata
759
+ shareId: link.Sharing?.ShareID || undefined,
760
+ isShared: !!link.Sharing,
761
+ isSharedPublicly: !!link.Sharing?.ShareURLID,
762
+ directRole: isAdmin ? MemberRole.Admin : membershipRole,
763
+ membership: link.Membership
764
+ ? {
765
+ role: membershipRole,
766
+ inviteTime: new Date(link.Membership.InviteTime * 1000),
767
+ }
768
+ : undefined,
769
+ };
770
+
771
+ const baseCryptoNodeMetadata = {
772
+ signatureEmail: link.Link.SignatureEmail || undefined,
773
+ nameSignatureEmail: link.Link.NameSignatureEmail || undefined,
774
+ armoredKey: link.Link.NodeKey,
775
+ armoredNodePassphrase: link.Link.NodePassphrase,
776
+ armoredNodePassphraseSignature: link.Link.NodePassphraseSignature,
777
+ membership: link.Membership
778
+ ? {
779
+ inviterEmail: link.Membership.InviterEmail,
780
+ base64MemberSharePassphraseKeyPacket: link.Membership.MemberSharePassphraseKeyPacket,
781
+ armoredInviterSharePassphraseKeyPacketSignature:
782
+ link.Membership.InviterSharePassphraseKeyPacketSignature,
783
+ armoredInviteeSharePassphraseSessionKeySignature:
784
+ link.Membership.InviteeSharePassphraseSessionKeySignature,
785
+ }
786
+ : undefined,
787
+ };
788
+
789
+ return {
790
+ baseNodeMetadata,
791
+ baseCryptoNodeMetadata,
792
+ };
793
+ }
794
+
721
795
  export function* groupNodeUidsByVolumeAndIteratePerBatch(
722
796
  nodeUids: string[],
723
797
  ): Generator<{ volumeId: string; batchNodeIds: string[]; batchNodeUids: string[] }> {
@@ -745,7 +819,6 @@ export function* groupNodeUidsByVolumeAndIteratePerBatch(
745
819
  }
746
820
  }
747
821
 
748
-
749
822
  function transformRevisionResponse(
750
823
  volumeId: string,
751
824
  nodeId: string,
@@ -23,6 +23,7 @@ function generateNode(
23
23
  isShared: false,
24
24
  isSharedPublicly: false,
25
25
  creationTime: new Date(),
26
+ modificationTime: new Date(),
26
27
  trashTime: undefined,
27
28
  volumeId: 'volumeId',
28
29
  isStale: false,
@@ -9,7 +9,9 @@ export enum CACHE_TAG_KEYS {
9
9
  Roots = 'nodeRoot',
10
10
  }
11
11
 
12
- type DecryptedNodeResult = { uid: string; ok: true; node: DecryptedNode } | { uid: string; ok: false; error: string };
12
+ type DecryptedNodeResult<T extends DecryptedNode> =
13
+ | { uid: string; ok: true; node: T }
14
+ | { uid: string; ok: false; error: string };
13
15
 
14
16
  /**
15
17
  * Provides caching for nodes metadata.
@@ -19,7 +21,7 @@ type DecryptedNodeResult = { uid: string; ok: true; node: DecryptedNode } | { ui
19
21
  *
20
22
  * The cache of node metadata should not contain any crypto material.
21
23
  */
22
- export class NodesCache {
24
+ export abstract class NodesCacheBase<T extends DecryptedNode = DecryptedNode> {
23
25
  constructor(
24
26
  private logger: Logger,
25
27
  private driveCache: ProtonDriveEntitiesCache,
@@ -28,9 +30,9 @@ export class NodesCache {
28
30
  this.driveCache = driveCache;
29
31
  }
30
32
 
31
- async setNode(node: DecryptedNode): Promise<void> {
33
+ async setNode(node: T): Promise<void> {
32
34
  const key = getCacheUid(node.uid);
33
- const nodeData = serialiseNode(node);
35
+ const nodeData = this.serialiseNode(node);
34
36
  const { volumeId } = splitNodeUid(node.uid);
35
37
 
36
38
  const tags = [`volume:${volumeId}`];
@@ -46,11 +48,11 @@ export class NodesCache {
46
48
  await this.driveCache.setEntity(key, nodeData, tags);
47
49
  }
48
50
 
49
- async getNode(nodeUid: string): Promise<DecryptedNode> {
51
+ async getNode(nodeUid: string): Promise<T> {
50
52
  const key = getCacheUid(nodeUid);
51
53
  const nodeData = await this.driveCache.getEntity(key);
52
54
  try {
53
- return deserialiseNode(nodeData);
55
+ return this.deserialiseNode(nodeData);
54
56
  } catch (error: unknown) {
55
57
  await this.removeCorruptedNode({ nodeUid }, error);
56
58
  throw new Error(`Failed to deserialise node: ${error instanceof Error ? error.message : error}`, {
@@ -59,6 +61,10 @@ export class NodesCache {
59
61
  }
60
62
  }
61
63
 
64
+ protected abstract serialiseNode(node: T): string;
65
+
66
+ protected abstract deserialiseNode(nodeData: string): T;
67
+
62
68
  /**
63
69
  * Set all nodes on given node as stale. This is useful when we
64
70
  * get refresh event from the server and we thus don't know
@@ -146,7 +152,7 @@ export class NodesCache {
146
152
  return cacheUids;
147
153
  }
148
154
 
149
- async *iterateNodes(nodeUids: string[]): AsyncGenerator<DecryptedNodeResult> {
155
+ async *iterateNodes(nodeUids: string[]): AsyncGenerator<DecryptedNodeResult<T>> {
150
156
  const cacheUids = nodeUids.map(getCacheUid);
151
157
  for await (const result of this.driveCache.iterateEntities(cacheUids)) {
152
158
  const node = await this.convertCacheResult(result);
@@ -156,7 +162,7 @@ export class NodesCache {
156
162
  }
157
163
  }
158
164
 
159
- async *iterateChildren(parentNodeUid: string): AsyncGenerator<DecryptedNodeResult> {
165
+ async *iterateChildren(parentNodeUid: string): AsyncGenerator<DecryptedNodeResult<T>> {
160
166
  for await (const result of this.driveCache.iterateEntitiesByTag(
161
167
  `${CACHE_TAG_KEYS.ParentUid}:${parentNodeUid}`,
162
168
  )) {
@@ -171,7 +177,7 @@ export class NodesCache {
171
177
  yield* this.driveCache.iterateEntitiesByTag(`${CACHE_TAG_KEYS.Roots}:${volumeId}`);
172
178
  }
173
179
 
174
- async *iterateTrashedNodes(): AsyncGenerator<DecryptedNodeResult> {
180
+ async *iterateTrashedNodes(): AsyncGenerator<DecryptedNodeResult<T>> {
175
181
  for await (const result of this.driveCache.iterateEntitiesByTag(CACHE_TAG_KEYS.Trashed)) {
176
182
  const node = await this.convertCacheResult(result);
177
183
  if (node) {
@@ -184,7 +190,7 @@ export class NodesCache {
184
190
  * Converts result from the cache with cache UID and data to result of node
185
191
  * with node UID and DecryptedNode.
186
192
  */
187
- private async convertCacheResult(result: EntityResult<string>): Promise<DecryptedNodeResult | null> {
193
+ private async convertCacheResult(result: EntityResult<string>): Promise<DecryptedNodeResult<T> | null> {
188
194
  let nodeUid;
189
195
  try {
190
196
  nodeUid = getNodeUid(result.key);
@@ -195,7 +201,7 @@ export class NodesCache {
195
201
  if (result.ok) {
196
202
  let node;
197
203
  try {
198
- node = deserialiseNode(result.value);
204
+ node = this.deserialiseNode(result.value);
199
205
  } catch (error: unknown) {
200
206
  await this.removeCorruptedNode({ nodeUid }, error);
201
207
  return null;
@@ -232,6 +238,16 @@ export class NodesCache {
232
238
  }
233
239
  }
234
240
 
241
+ export class NodesCache extends NodesCacheBase<DecryptedNode> {
242
+ protected serialiseNode(node: DecryptedNode): string {
243
+ return serialiseNode(node);
244
+ }
245
+
246
+ protected deserialiseNode(nodeData: string): DecryptedNode {
247
+ return deserialiseNode(nodeData);
248
+ }
249
+ }
250
+
235
251
  function getCacheUid(nodeUid: string) {
236
252
  return `node-${nodeUid}`;
237
253
  }
@@ -243,11 +259,12 @@ function getNodeUid(cacheUid: string) {
243
259
  return cacheUid.substring(5);
244
260
  }
245
261
 
246
- function serialiseNode(node: DecryptedNode) {
262
+ export function serialiseNode(node: DecryptedNode) {
247
263
  return JSON.stringify(node);
248
264
  }
249
265
 
250
- function deserialiseNode(nodeData: string): DecryptedNode {
266
+ // TODO: use better deserialisation with validation
267
+ export function deserialiseNode(nodeData: string): DecryptedNode {
251
268
  const node = JSON.parse(nodeData);
252
269
  if (
253
270
  !node ||
@@ -263,6 +280,7 @@ function deserialiseNode(nodeData: string): DecryptedNode {
263
280
  typeof node.isShared !== 'boolean' ||
264
281
  !node.creationTime ||
265
282
  typeof node.creationTime !== 'string' ||
283
+ typeof node.modificationTime !== 'string' ||
266
284
  (typeof node.trashTime !== 'string' && node.trashTime !== undefined) ||
267
285
  (typeof node.folder !== 'object' && node.folder !== undefined) ||
268
286
  (typeof node.folder?.claimedModificationTime !== 'string' && node.folder?.claimedModificationTime !== undefined)
@@ -272,6 +290,7 @@ function deserialiseNode(nodeData: string): DecryptedNode {
272
290
  return {
273
291
  ...node,
274
292
  creationTime: new Date(node.creationTime),
293
+ modificationTime: new Date(node.modificationTime),
275
294
  trashTime: node.trashTime ? new Date(node.trashTime) : undefined,
276
295
  activeRevision: node.activeRevision ? deserialiseRevision(node.activeRevision) : undefined,
277
296
  membership: node.membership
@@ -1327,7 +1327,12 @@ describe('nodesCryptoService', () => {
1327
1327
  key: 'addressKey' as any,
1328
1328
  };
1329
1329
 
1330
- const result = await cryptoService.encryptNodeWithNewParent(node, keys as any, parentKeys, signingKeys);
1330
+ const result = await cryptoService.encryptNodeWithNewParent(
1331
+ node.name,
1332
+ keys as any,
1333
+ parentKeys,
1334
+ signingKeys,
1335
+ );
1331
1336
 
1332
1337
  expect(result).toEqual({
1333
1338
  encryptedName: 'encryptedNodeName',
@@ -1360,7 +1365,12 @@ describe('nodesCryptoService', () => {
1360
1365
  parentNodeKey: 'parentNodeKey' as any,
1361
1366
  };
1362
1367
 
1363
- const result = await cryptoService.encryptNodeWithNewParent(node, keys as any, parentKeys, signingKeys);
1368
+ const result = await cryptoService.encryptNodeWithNewParent(
1369
+ node.name,
1370
+ keys as any,
1371
+ parentKeys,
1372
+ signingKeys,
1373
+ );
1364
1374
 
1365
1375
  expect(result).toEqual({
1366
1376
  encryptedName: 'encryptedNodeName',
@@ -1407,7 +1417,7 @@ describe('nodesCryptoService', () => {
1407
1417
  };
1408
1418
 
1409
1419
  await expect(
1410
- cryptoService.encryptNodeWithNewParent(node, keys as any, parentKeys, signingKeys),
1420
+ cryptoService.encryptNodeWithNewParent(node.name, keys as any, parentKeys, signingKeys),
1411
1421
  ).rejects.toThrow('Moving item to a non-folder is not allowed');
1412
1422
  });
1413
1423
 
@@ -637,7 +637,7 @@ export class NodesCryptoService {
637
637
  }
638
638
 
639
639
  async encryptNodeWithNewParent(
640
- node: Pick<DecryptedNode, 'name'>,
640
+ nodeName: DecryptedNode['name'],
641
641
  keys: { passphrase: string; passphraseSessionKey: SessionKey; nameSessionKey: SessionKey },
642
642
  parentKeys: { key: PrivateKey; hashKey: Uint8Array },
643
643
  signingKeys: NodeSigningKeys,
@@ -652,7 +652,7 @@ export class NodesCryptoService {
652
652
  if (!parentKeys.hashKey) {
653
653
  throw new ValidationError('Moving item to a non-folder is not allowed');
654
654
  }
655
- if (!node.name.ok) {
655
+ if (!nodeName.ok) {
656
656
  throw new ValidationError('Cannot move item without a valid name, please rename the item first');
657
657
  }
658
658
 
@@ -664,12 +664,12 @@ export class NodesCryptoService {
664
664
  }
665
665
 
666
666
  const { armoredNodeName } = await this.driveCrypto.encryptNodeName(
667
- node.name.value,
667
+ nodeName.value,
668
668
  keys.nameSessionKey,
669
669
  parentKeys.key,
670
670
  nameAndPassprhaseSigningKey,
671
671
  );
672
- const hash = await this.driveCrypto.generateLookupHash(node.name.value, parentKeys.hashKey);
672
+ const hash = await this.driveCrypto.generateLookupHash(nodeName.value, parentKeys.hashKey);
673
673
  const { armoredPassphrase, armoredPassphraseSignature } = await this.driveCrypto.encryptPassphrase(
674
674
  keys.passphrase,
675
675
  keys.passphraseSessionKey,
@@ -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> {