@protontech/drive-sdk 0.0.12 → 0.0.13

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 (89) hide show
  1. package/dist/errors.d.ts +7 -3
  2. package/dist/errors.js +9 -4
  3. package/dist/errors.js.map +1 -1
  4. package/dist/interface/index.d.ts +1 -1
  5. package/dist/interface/nodes.d.ts +12 -1
  6. package/dist/interface/nodes.js +11 -0
  7. package/dist/interface/nodes.js.map +1 -1
  8. package/dist/interface/upload.d.ts +51 -3
  9. package/dist/internal/apiService/driveTypes.d.ts +1341 -465
  10. package/dist/internal/apiService/errors.js +2 -2
  11. package/dist/internal/apiService/errors.js.map +1 -1
  12. package/dist/internal/apiService/transformers.js +2 -0
  13. package/dist/internal/apiService/transformers.js.map +1 -1
  14. package/dist/internal/asyncIteratorMap.d.ts +15 -0
  15. package/dist/internal/asyncIteratorMap.js +59 -0
  16. package/dist/internal/asyncIteratorMap.js.map +1 -0
  17. package/dist/internal/asyncIteratorMap.test.d.ts +1 -0
  18. package/dist/internal/asyncIteratorMap.test.js +120 -0
  19. package/dist/internal/asyncIteratorMap.test.js.map +1 -0
  20. package/dist/internal/nodes/apiService.d.ts +2 -2
  21. package/dist/internal/nodes/apiService.js +16 -6
  22. package/dist/internal/nodes/apiService.js.map +1 -1
  23. package/dist/internal/nodes/apiService.test.js +30 -8
  24. package/dist/internal/nodes/apiService.test.js.map +1 -1
  25. package/dist/internal/nodes/cache.js +1 -0
  26. package/dist/internal/nodes/cache.js.map +1 -1
  27. package/dist/internal/nodes/cache.test.js +1 -0
  28. package/dist/internal/nodes/cache.test.js.map +1 -1
  29. package/dist/internal/nodes/cryptoService.test.js +34 -0
  30. package/dist/internal/nodes/cryptoService.test.js.map +1 -1
  31. package/dist/internal/nodes/index.test.js +3 -1
  32. package/dist/internal/nodes/index.test.js.map +1 -1
  33. package/dist/internal/nodes/interface.d.ts +3 -1
  34. package/dist/internal/nodes/nodesAccess.js +28 -7
  35. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  36. package/dist/internal/nodes/nodesAccess.test.js +7 -6
  37. package/dist/internal/nodes/nodesAccess.test.js.map +1 -1
  38. package/dist/internal/sharing/apiService.js +19 -2
  39. package/dist/internal/sharing/apiService.js.map +1 -1
  40. package/dist/internal/upload/fileUploader.d.ts +49 -53
  41. package/dist/internal/upload/fileUploader.js +91 -395
  42. package/dist/internal/upload/fileUploader.js.map +1 -1
  43. package/dist/internal/upload/fileUploader.test.js +38 -292
  44. package/dist/internal/upload/fileUploader.test.js.map +1 -1
  45. package/dist/internal/upload/index.d.ts +3 -3
  46. package/dist/internal/upload/index.js +20 -41
  47. package/dist/internal/upload/index.js.map +1 -1
  48. package/dist/internal/upload/manager.d.ts +1 -1
  49. package/dist/internal/upload/manager.js +16 -19
  50. package/dist/internal/upload/manager.js.map +1 -1
  51. package/dist/internal/upload/manager.test.js +42 -83
  52. package/dist/internal/upload/manager.test.js.map +1 -1
  53. package/dist/internal/upload/streamUploader.d.ts +62 -0
  54. package/dist/internal/upload/streamUploader.js +441 -0
  55. package/dist/internal/upload/streamUploader.js.map +1 -0
  56. package/dist/internal/upload/streamUploader.test.d.ts +1 -0
  57. package/dist/internal/upload/streamUploader.test.js +358 -0
  58. package/dist/internal/upload/streamUploader.test.js.map +1 -0
  59. package/dist/protonDriveClient.d.ts +4 -4
  60. package/dist/protonDriveClient.js +1 -1
  61. package/dist/protonDriveClient.js.map +1 -1
  62. package/package.json +2 -2
  63. package/src/errors.ts +10 -4
  64. package/src/interface/index.ts +1 -1
  65. package/src/interface/nodes.ts +11 -0
  66. package/src/interface/upload.ts +53 -3
  67. package/src/internal/apiService/driveTypes.ts +1341 -465
  68. package/src/internal/apiService/errors.ts +3 -2
  69. package/src/internal/apiService/transformers.ts +2 -0
  70. package/src/internal/asyncIteratorMap.test.ts +150 -0
  71. package/src/internal/asyncIteratorMap.ts +64 -0
  72. package/src/internal/nodes/apiService.test.ts +36 -7
  73. package/src/internal/nodes/apiService.ts +19 -7
  74. package/src/internal/nodes/cache.test.ts +1 -0
  75. package/src/internal/nodes/cache.ts +1 -0
  76. package/src/internal/nodes/cryptoService.test.ts +38 -0
  77. package/src/internal/nodes/index.test.ts +3 -1
  78. package/src/internal/nodes/interface.ts +4 -1
  79. package/src/internal/nodes/nodesAccess.test.ts +7 -6
  80. package/src/internal/nodes/nodesAccess.ts +30 -7
  81. package/src/internal/sharing/apiService.ts +24 -2
  82. package/src/internal/upload/fileUploader.test.ts +46 -376
  83. package/src/internal/upload/fileUploader.ts +114 -494
  84. package/src/internal/upload/index.ts +26 -50
  85. package/src/internal/upload/manager.test.ts +45 -92
  86. package/src/internal/upload/manager.ts +30 -32
  87. package/src/internal/upload/streamUploader.test.ts +469 -0
  88. package/src/internal/upload/streamUploader.ts +552 -0
  89. package/src/protonDriveClient.ts +5 -4
@@ -14,6 +14,7 @@ export function apiErrorFactory({ response, result }: { response: Response, resu
14
14
  const typedResult = result as {
15
15
  Code?: number;
16
16
  Error?: string;
17
+ Details?: object;
17
18
  exception?: string;
18
19
  message?: string;
19
20
  file?: string;
@@ -21,7 +22,7 @@ export function apiErrorFactory({ response, result }: { response: Response, resu
21
22
  trace?: object;
22
23
  };
23
24
 
24
- const [code, message] = [typedResult.Code || 0, typedResult.Error || c('Error').t`Unknown error`];
25
+ const [code, message, details] = [typedResult.Code || 0, typedResult.Error || c('Error').t`Unknown error`, typedResult.Details];
25
26
 
26
27
  const debug = typedResult.exception ? {
27
28
  exception: typedResult.exception,
@@ -55,7 +56,7 @@ export function apiErrorFactory({ response, result }: { response: Response, resu
55
56
  case ErrorCode.INSUFFICIENT_SHARE_QUOTA:
56
57
  case ErrorCode.INSUFFICIENT_SHARE_JOINED_QUOTA:
57
58
  case ErrorCode.INSUFFICIENT_BOOKMARKS_QUOTA:
58
- return new ValidationError(message, code);
59
+ return new ValidationError(message, code, details);
59
60
  default:
60
61
  return new APICodeError(message, code, debug);
61
62
  }
@@ -6,6 +6,8 @@ export function nodeTypeNumberToNodeType(logger: Logger, nodeTypeNumber: number)
6
6
  return NodeType.Folder;
7
7
  case 2:
8
8
  return NodeType.File;
9
+ case 3:
10
+ return NodeType.Album;
9
11
  default:
10
12
  logger.warn(`Unknown node type: ${nodeTypeNumber}`);
11
13
  return NodeType.File;
@@ -0,0 +1,150 @@
1
+ import { asyncIteratorMap } from './asyncIteratorMap';
2
+
3
+ // Helper function to create an async generator from array
4
+ async function* createAsyncGenerator<T>(items: T[]): AsyncGenerator<T> {
5
+ for (const item of items) {
6
+ yield item;
7
+ }
8
+ }
9
+
10
+ // Helper function to collect all results from async generator
11
+ async function collectResults<T>(asyncGen: AsyncGenerator<T>): Promise<T[]> {
12
+ const results: T[] = [];
13
+ for await (const item of asyncGen) {
14
+ results.push(item);
15
+ }
16
+ return results;
17
+ }
18
+
19
+ describe('asyncIteratorMap', () => {
20
+ test('works with empty input', async () => {
21
+ const inputGen = createAsyncGenerator([]);
22
+ const mapper = async (x: number) => x * 2;
23
+
24
+ const mappedGen = asyncIteratorMap(inputGen, mapper);
25
+ const results = await collectResults(mappedGen);
26
+
27
+ expect(results).toEqual([]);
28
+ });
29
+
30
+ test('works with single item', async () => {
31
+ const inputGen = createAsyncGenerator([42]);
32
+ const mapper = async (x: number) => x * 2;
33
+
34
+ const mappedGen = asyncIteratorMap(inputGen, mapper);
35
+ const results = await collectResults(mappedGen);
36
+
37
+ expect(results).toEqual([84]);
38
+ });
39
+
40
+ test('works with 5 values', async () => {
41
+ const inputGen = createAsyncGenerator([1, 2, 3, 4, 5]);
42
+ const mapper = async (x: number) => x * 2;
43
+
44
+ const mappedGen = asyncIteratorMap(inputGen, mapper);
45
+ const results = await collectResults(mappedGen);
46
+
47
+ expect(results).toEqual([2, 4, 6, 8, 10]);
48
+ });
49
+
50
+ test('works with slow mapper - finishes as fast as the longest delay', async () => {
51
+ const delays: { [key: number]: number } = { 1: 100, 2: 50, 3: 200, 4: 30, 5: 80 };
52
+ const inputGen = createAsyncGenerator(Object.keys(delays).map(Number));
53
+
54
+ const slowMapper = async (x: number) => {
55
+ await new Promise(resolve => setTimeout(resolve, delays[x]));
56
+ return x * 2;
57
+ };
58
+
59
+ const startTime = Date.now();
60
+ const mappedGen = asyncIteratorMap(inputGen, slowMapper, 5);
61
+ const results = await collectResults(mappedGen);
62
+ const endTime = Date.now();
63
+
64
+ // Should complete in roughly the time of the longest delay (200ms) plus some overhead
65
+ const executionTime = endTime - startTime;
66
+ expect(executionTime).toBeGreaterThanOrEqual(195); // We had failures with 199ms - JS is not precise.
67
+ expect(executionTime).toBeLessThan(250);
68
+
69
+ // Results should be in the order of the delays
70
+ expect(results).toEqual([8, 4, 10, 2, 6]);
71
+ });
72
+
73
+ test('handles errors from input iterator properly', async () => {
74
+ const throwingInputGen = async function*() {
75
+ yield 1;
76
+ yield 2;
77
+ throw new Error('Error providing value: 3');
78
+ }
79
+
80
+ const mapper = async (x: number) => x * 2;
81
+
82
+ const mappedGen = asyncIteratorMap(throwingInputGen(), mapper);
83
+
84
+ const results: number[] = [];
85
+ let caughtError: Error | null = null;
86
+
87
+ try {
88
+ for await (const item of mappedGen) {
89
+ results.push(item);
90
+ }
91
+ } catch (error) {
92
+ caughtError = error as Error;
93
+ }
94
+
95
+ expect(caughtError?.message).toBe('Error providing value: 3');
96
+ expect(results).toEqual([2, 4]);
97
+ });
98
+
99
+ test('handles errors from mapper properly', async () => {
100
+ const inputGen = createAsyncGenerator([1, 2, 3, 4, 5]);
101
+
102
+ const throwingMapper = async (x: number) => {
103
+ if (x === 3) {
104
+ throw new Error(`Error processing value: ${x}`);
105
+ }
106
+ return x * 2;
107
+ };
108
+
109
+ const mappedGen = asyncIteratorMap(inputGen, throwingMapper);
110
+
111
+ const results: number[] = [];
112
+ let caughtError: Error | null = null;
113
+
114
+ try {
115
+ for await (const item of mappedGen) {
116
+ results.push(item);
117
+ }
118
+ } catch (error) {
119
+ caughtError = error as Error;
120
+ }
121
+
122
+ expect(caughtError?.message).toBe('Error processing value: 3');
123
+ expect(results).toEqual([2, 4]);
124
+ });
125
+
126
+ test('respects concurrency limit', async () => {
127
+ const inputGen = createAsyncGenerator([1, 2, 3, 4, 5, 6, 7, 8]);
128
+
129
+ let concurrentExecutions = 0;
130
+ let maxConcurrentExecutions = 0;
131
+
132
+ const mapper = async (x: number) => {
133
+ concurrentExecutions++;
134
+ maxConcurrentExecutions = Math.max(maxConcurrentExecutions, concurrentExecutions);
135
+
136
+ // Wait for 100ms to simulate work
137
+ await new Promise(resolve => setTimeout(resolve, 100));
138
+
139
+ concurrentExecutions--;
140
+ return x * 2;
141
+ };
142
+
143
+ const concurrencyLimit = 3;
144
+ const mappedGen = asyncIteratorMap(inputGen, mapper, concurrencyLimit);
145
+ const results = await collectResults(mappedGen);
146
+
147
+ expect(maxConcurrentExecutions).toBe(concurrencyLimit);
148
+ expect(results).toEqual([2, 4, 6, 8, 10, 12, 14, 16]);
149
+ });
150
+ });
@@ -0,0 +1,64 @@
1
+ const DEFAULT_CONCURRENCY = 10;
2
+
3
+ /**
4
+ * Maps values from an input iterator and produces a new iterator.
5
+ * The mapper function is not awaited immediately to allow for parallel
6
+ * execution. The order of the items in the output iterator is not the
7
+ * same as the order of the items in the input iterator.
8
+ *
9
+ * Any error from the input iterator or the mapper function is propagated
10
+ * to the output iterator.
11
+ *
12
+ * @param inputIterator - The input async iterator.
13
+ * @param mapper - The mapper function that maps the input values to output values.
14
+ * @param concurrency - The concurrency limit. How many parallel async mapper calls are allowed.
15
+ * @returns An async iterator that yields the mapped values.
16
+ */
17
+ export async function* asyncIteratorMap<I, O>(
18
+ inputIterator: AsyncGenerator<I>,
19
+ mapper: (item: I) => Promise<O>,
20
+ concurrency: number = DEFAULT_CONCURRENCY,
21
+ ): AsyncGenerator<O> {
22
+ let done = false;
23
+
24
+ const executing = new Set<Promise<void>>();
25
+ const results: Array<Promise<O>> = [];
26
+
27
+ const pump = async () => {
28
+ let next;
29
+ try {
30
+ next = await inputIterator.next();
31
+ } catch (error) {
32
+ results.push(Promise.reject(error));
33
+ return;
34
+ }
35
+
36
+ if (next.done) {
37
+ done = true;
38
+ return;
39
+ }
40
+
41
+ const promise = mapper(next.value)
42
+ .then((result) => {
43
+ results.push(Promise.resolve(result));
44
+ })
45
+ .catch((error) => {
46
+ results.push(Promise.reject(error));
47
+ });
48
+ executing.add(promise);
49
+ void promise.finally(() => executing.delete(promise));
50
+ };
51
+
52
+ while (!done || executing.size > 0 || results.length > 0) {
53
+ while (!done && executing.size < concurrency) {
54
+ await pump();
55
+ }
56
+
57
+ if (results.length > 0) {
58
+ yield await results.shift()!;
59
+ } else if (executing.size > 0) {
60
+ // Wait for at least one task to complete
61
+ await Promise.race(Array.from(executing));
62
+ }
63
+ }
64
+ }
@@ -44,6 +44,18 @@ function generateAPIFolderNode(linkOverrides = {}, overrides = {}) {
44
44
  };
45
45
  }
46
46
 
47
+ function generateAPIAlbumNode(linkOverrides = {}, overrides = {}) {
48
+ const node = generateAPINode();
49
+ return {
50
+ Link: {
51
+ ...node.Link,
52
+ Type: 3,
53
+ ...linkOverrides,
54
+ },
55
+ ...overrides,
56
+ };
57
+ }
58
+
47
59
  function generateAPINode() {
48
60
  return {
49
61
  Link: {
@@ -107,6 +119,15 @@ function generateFolderNode(overrides = {}) {
107
119
  }
108
120
  }
109
121
 
122
+ function generateAlbumNode(overrides = {}) {
123
+ const node = generateNode();
124
+ return {
125
+ ...node,
126
+ type: NodeType.Album,
127
+ ...overrides
128
+ }
129
+ }
130
+
110
131
  function generateNode() {
111
132
  return {
112
133
  hash: "nameHash",
@@ -119,7 +140,7 @@ function generateNode() {
119
140
 
120
141
  shareId: undefined,
121
142
  isShared: false,
122
- directMemberRole: MemberRole.Inherited,
143
+ directMemberRole: MemberRole.Admin,
123
144
 
124
145
  encryptedCrypto: {
125
146
  armoredKey: "nodeKey",
@@ -149,13 +170,13 @@ describe("nodeAPIService", () => {
149
170
  });
150
171
 
151
172
  describe('iterateNodes', () => {
152
- async function testIterateNodes(mockedLink: any, expectedNode: any) {
173
+ async function testIterateNodes(mockedLink: any, expectedNode: any, ownVolumeId = 'volumeId') {
153
174
  // @ts-expect-error Mocking for testing purposes
154
175
  apiMock.post = jest.fn(async () => Promise.resolve({
155
176
  Links: [mockedLink],
156
177
  }));
157
178
 
158
- const nodes = await Array.fromAsync(api.iterateNodes(['volumeId~nodeId']));
179
+ const nodes = await Array.fromAsync(api.iterateNodes(['volumeId~nodeId'], ownVolumeId));
159
180
  expect(nodes).toStrictEqual([expectedNode]);
160
181
  }
161
182
 
@@ -180,6 +201,13 @@ describe("nodeAPIService", () => {
180
201
  );
181
202
  });
182
203
 
204
+ it('should get album node', async () => {
205
+ await testIterateNodes(
206
+ generateAPIAlbumNode(),
207
+ generateAlbumNode(),
208
+ );
209
+ });
210
+
183
211
  it('should get shared node', async () => {
184
212
  await testIterateNodes(
185
213
  generateAPIFolderNode({}, {
@@ -213,6 +241,7 @@ describe("nodeAPIService", () => {
213
241
  shareId: 'shareId',
214
242
  directMemberRole: MemberRole.Viewer,
215
243
  }),
244
+ 'myVolumeId',
216
245
  );
217
246
  });
218
247
 
@@ -240,7 +269,7 @@ describe("nodeAPIService", () => {
240
269
  ],
241
270
  }));
242
271
 
243
- const generator = api.iterateNodes(['volumeId~nodeId']);
272
+ const generator = api.iterateNodes(['volumeId~nodeId'], 'volumeId');
244
273
 
245
274
  const node1 = await generator.next();
246
275
  expect(node1.value).toStrictEqual(generateFolderNode());
@@ -272,10 +301,10 @@ describe("nodeAPIService", () => {
272
301
  ],
273
302
  }));
274
303
 
275
- const nodes = await Array.fromAsync(api.iterateNodes(['volumeId1~nodeId1', 'volumeId2~nodeId2']));
304
+ const nodes = await Array.fromAsync(api.iterateNodes(['volumeId1~nodeId1', 'volumeId2~nodeId2'], 'volumeId1'));
276
305
  expect(nodes).toStrictEqual([
277
- generateFolderNode({ uid: 'volumeId1~nodeId1', parentUid: 'volumeId1~parentNodeId1' }),
278
- generateFolderNode({ uid: 'volumeId2~nodeId2', parentUid: 'volumeId2~parentNodeId2' }),
306
+ generateFolderNode({ uid: 'volumeId1~nodeId1', parentUid: 'volumeId1~parentNodeId1', directMemberRole: MemberRole.Admin }),
307
+ generateFolderNode({ uid: 'volumeId2~nodeId2', parentUid: 'volumeId2~parentNodeId2', directMemberRole: MemberRole.Inherited }),
279
308
  ]);
280
309
  });
281
310
  });
@@ -2,7 +2,7 @@ import { c } from "ttag";
2
2
 
3
3
  import { ProtonDriveError, ValidationError } from "../../errors";
4
4
  import { Logger, NodeResult } from "../../interface";
5
- import { RevisionState } from "../../interface/nodes";
5
+ import { MemberRole, RevisionState } from "../../interface/nodes";
6
6
  import { DriveAPIService, drivePaths, isCodeOk, nodeTypeNumberToNodeType, permissionsToDirectMemberRole } from "../apiService";
7
7
  import { splitNodeUid, makeNodeUid, makeNodeRevisionUid, splitNodeRevisionUid, makeNodeThumbnailUid } from "../uids";
8
8
  import { EncryptedNode, EncryptedRevision, Thumbnail } from "./interface";
@@ -56,15 +56,15 @@ export class NodeAPIService {
56
56
  this.apiService = apiService;
57
57
  }
58
58
 
59
- async getNode(nodeUid: string, signal?: AbortSignal): Promise<EncryptedNode> {
60
- const nodesGenerator = this.iterateNodes([nodeUid], signal);
59
+ async getNode(nodeUid: string, ownVolumeId: string, signal?: AbortSignal): Promise<EncryptedNode> {
60
+ const nodesGenerator = this.iterateNodes([nodeUid], ownVolumeId, signal);
61
61
  const result = await nodesGenerator.next();
62
62
  await nodesGenerator.return("finish");
63
63
  return result.value;
64
64
  }
65
65
 
66
66
  // Improvement requested: split into multiple calls for many nodes.
67
- async* iterateNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<EncryptedNode> {
67
+ async* iterateNodes(nodeUids: string[], ownVolumeId: string, signal?: AbortSignal): AsyncGenerator<EncryptedNode> {
68
68
  const allNodeIds = nodeUids.map(splitNodeUid);
69
69
 
70
70
  const nodeIdsByVolumeId = new Map<string, string[]>();
@@ -81,13 +81,15 @@ export class NodeAPIService {
81
81
  const errors = [];
82
82
 
83
83
  for (const [volumeId, nodeIds] of nodeIdsByVolumeId.entries()) {
84
+ const isAdmin = volumeId === ownVolumeId;
85
+
84
86
  const response = await this.apiService.post<PostLoadLinksMetadataRequest, PostLoadLinksMetadataResponse>(`drive/v2/volumes/${volumeId}/links`, {
85
87
  LinkIDs: nodeIds,
86
88
  }, signal);
87
89
 
88
90
  for (const link of response.Links) {
89
91
  try {
90
- yield linkToEncryptedNode(this.logger, volumeId, link);
92
+ yield linkToEncryptedNode(this.logger, volumeId, link, isAdmin);
91
93
  } catch (error: unknown) {
92
94
  this.logger.error(`Failed to transform node ${link.Link.LinkID}`, error);
93
95
  errors.push(error);
@@ -363,7 +365,7 @@ function* handleResponseErrors(nodeUids: string[], volumeId: string, responses:
363
365
  }
364
366
  }
365
367
 
366
- function linkToEncryptedNode(logger: Logger, volumeId: string, link: PostLoadLinksMetadataResponse['Links'][0]): EncryptedNode {
368
+ function linkToEncryptedNode(logger: Logger, volumeId: string, link: PostLoadLinksMetadataResponse['Links'][0], isAdmin: boolean): EncryptedNode {
367
369
  const baseNodeMetadata = {
368
370
  // Internal metadata
369
371
  hash: link.Link.NameHash || undefined,
@@ -379,7 +381,7 @@ function linkToEncryptedNode(logger: Logger, volumeId: string, link: PostLoadLin
379
381
  // Sharing node metadata
380
382
  shareId: link.Sharing?.ShareID || undefined,
381
383
  isShared: !!link.Sharing,
382
- directMemberRole: permissionsToDirectMemberRole(logger, link.Membership?.Permissions),
384
+ directMemberRole: isAdmin ? MemberRole.Admin : permissionsToDirectMemberRole(logger, link.Membership?.Permissions),
383
385
  }
384
386
  const baseCryptoNodeMetadata = {
385
387
  signatureEmail: link.Link.SignatureEmail || undefined,
@@ -426,6 +428,15 @@ function linkToEncryptedNode(logger: Logger, volumeId: string, link: PostLoadLin
426
428
  }
427
429
  }
428
430
 
431
+ if (link.Link.Type === 3) {
432
+ return {
433
+ ...baseNodeMetadata,
434
+ encryptedCrypto: {
435
+ ...baseCryptoNodeMetadata,
436
+ },
437
+ }
438
+ }
439
+
429
440
  throw new Error(`Unknown node type: ${link.Link.Type}`);
430
441
  }
431
442
 
@@ -437,6 +448,7 @@ function transformRevisionResponse(
437
448
  return {
438
449
  uid: makeNodeRevisionUid(volumeId, nodeId, revision.ID),
439
450
  state: revision.State === APIRevisionState.Active ? RevisionState.Active : RevisionState.Superseded,
451
+ // @ts-expect-error: API doc is wrong, CreateTime is not optional.
440
452
  creationTime: new Date(revision.CreateTime*1000),
441
453
  storageSize: revision.Size,
442
454
  signatureEmail: revision.SignatureEmail || undefined,
@@ -108,6 +108,7 @@ describe('nodesCache', () => {
108
108
  creationTime: new Date('2021-01-01'),
109
109
  storageSize: 100,
110
110
  contentAuthor: resultOk('test@test.com'),
111
+ claimedModificationTime: new Date('2021-02-01')
111
112
  });
112
113
  const node = generateNode('node1', '', { activeRevision });
113
114
 
@@ -261,6 +261,7 @@ function deserialiseRevision(revision: any): Result<DecryptedRevision, Error> {
261
261
  return resultOk({
262
262
  ...revision.value,
263
263
  creationTime: new Date(revision.value.creationTime),
264
+ claimedModificationTime: new Date(revision.value.claimedModificationTime)
264
265
  });
265
266
  }
266
267
 
@@ -578,6 +578,44 @@ describe("nodesCryptoService", () => {
578
578
  });
579
579
  });
580
580
 
581
+ describe("album node", () => {
582
+ const encryptedNode = {
583
+ uid: "volumeId~nodeId",
584
+ parentUid: "volumeId~parentId",
585
+ encryptedCrypto: {
586
+ signatureEmail: "signatureEmail",
587
+ nameSignatureEmail: "nameSignatureEmail",
588
+ armoredKey: "armoredKey",
589
+ armoredNodePassphrase: "armoredNodePassphrase",
590
+ armoredNodePassphraseSignature: "armoredNodePassphraseSignature",
591
+ },
592
+ } as EncryptedNode;
593
+
594
+ it("should decrypt successfuly", async () => {
595
+ const result = await cryptoService.decryptNode(encryptedNode, parentKey);
596
+
597
+ expect(result).toMatchObject({
598
+ node: {
599
+ name: { ok: true, value: "name" },
600
+ keyAuthor: { ok: true, value: "signatureEmail" },
601
+ nameAuthor: { ok: true, value: "nameSignatureEmail" },
602
+ folder: undefined,
603
+ activeRevision: undefined,
604
+ errors: undefined,
605
+ },
606
+ keys: {
607
+ passphrase: "pass",
608
+ key: "decryptedKey",
609
+ passphraseSessionKey: "passphraseSessionKey",
610
+ hashKey: new Uint8Array(),
611
+ }
612
+ });
613
+
614
+ expect(account.getPublicKeys).toHaveBeenCalledTimes(2);
615
+ expect(telemetry.logEvent).not.toHaveBeenCalled();
616
+ });
617
+ });
618
+
581
619
  describe("anonymous node", () => {
582
620
  const encryptedNode = {
583
621
  uid: "volumeId~nodeId",
@@ -55,7 +55,9 @@ describe('nodesModules integration tests', () => {
55
55
  }),
56
56
  }
57
57
  // @ts-expect-error No need to implement all methods for mocking
58
- sharesService = {}
58
+ sharesService = {
59
+ getMyFilesIDs: jest.fn().mockResolvedValue({ volumeId: 'volumeId' }),
60
+ }
59
61
 
60
62
  nodesModule = initNodesModule(
61
63
  getMockTelemetry(),
@@ -30,7 +30,7 @@ interface BaseNode {
30
30
  * Outside of the module, the decrypted node interface should be used.
31
31
  */
32
32
  export interface EncryptedNode extends BaseNode {
33
- encryptedCrypto: EncryptedNodeFolderCrypto | EncryptedNodeFileCrypto;
33
+ encryptedCrypto: EncryptedNodeFolderCrypto | EncryptedNodeFileCrypto | EncryptedNodeAlbumCrypto;
34
34
  }
35
35
 
36
36
  export interface EncryptedNodeCrypto {
@@ -56,6 +56,9 @@ export interface EncryptedNodeFolderCrypto extends EncryptedNodeCrypto {
56
56
  };
57
57
  }
58
58
 
59
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
60
+ export interface EncryptedNodeAlbumCrypto extends EncryptedNodeCrypto {}
61
+
59
62
  /**
60
63
  * Interface used only internally in the nodes module.
61
64
  *
@@ -46,6 +46,7 @@ describe('nodesAccess', () => {
46
46
  }
47
47
  // @ts-expect-error No need to implement all methods for mocking
48
48
  shareService = {
49
+ getMyFilesIDs: jest.fn().mockResolvedValue({ volumeId: 'volumeId' }),
49
50
  getSharePrivateKey: jest.fn(),
50
51
  };
51
52
 
@@ -81,7 +82,7 @@ describe('nodesAccess', () => {
81
82
 
82
83
  const result = await access.getNode('nodeId');
83
84
  expect(result).toEqual(decryptedNode);
84
- expect(apiService.getNode).toHaveBeenCalledWith('nodeId');
85
+ expect(apiService.getNode).toHaveBeenCalledWith('nodeId', 'volumeId');
85
86
  expect(cryptoCache.getNodeKeys).toHaveBeenCalledWith('parentUid');
86
87
  expect(cryptoService.decryptNode).toHaveBeenCalledWith(encryptedNode, 'parentKey');
87
88
  expect(cache.setNode).toHaveBeenCalledWith(decryptedNode);
@@ -107,7 +108,7 @@ describe('nodesAccess', () => {
107
108
 
108
109
  const result = await access.getNode('nodeId');
109
110
  expect(result).toEqual(decryptedNode);
110
- expect(apiService.getNode).toHaveBeenCalledWith('nodeId');
111
+ expect(apiService.getNode).toHaveBeenCalledWith('nodeId', 'volumeId');
111
112
  expect(cryptoCache.getNodeKeys).toHaveBeenCalledWith('parentUid');
112
113
  expect(cryptoService.decryptNode).toHaveBeenCalledWith(encryptedNode, 'parentKey');
113
114
  expect(cache.setNode).toHaveBeenCalledWith(decryptedNode);
@@ -179,7 +180,7 @@ describe('nodesAccess', () => {
179
180
 
180
181
  const result = await Array.fromAsync(access.iterateFolderChildren('parentUid'));
181
182
  expect(result).toMatchObject([node1, node4, node2, node3]);
182
- expect(apiService.iterateNodes).toHaveBeenCalledWith(['node2', 'node3'], undefined);
183
+ expect(apiService.iterateNodes).toHaveBeenCalledWith(['node2', 'node3'], 'volumeId', undefined);
183
184
  expect(cryptoService.decryptNode).toHaveBeenCalledTimes(2);
184
185
  expect(cache.setNode).toHaveBeenCalledTimes(2);
185
186
  expect(cryptoCache.setNodeKeys).toHaveBeenCalledTimes(2);
@@ -218,7 +219,7 @@ describe('nodesAccess', () => {
218
219
  const result = await Array.fromAsync(access.iterateFolderChildren('parentUid'));
219
220
  expect(result).toMatchObject([node1, node2, node3, node4]);
220
221
  expect(apiService.iterateChildrenNodeUids).toHaveBeenCalledWith('parentUid', undefined);
221
- expect(apiService.iterateNodes).toHaveBeenCalledWith(['node1', 'node2', 'node3', 'node4'], undefined);
222
+ expect(apiService.iterateNodes).toHaveBeenCalledWith(['node1', 'node2', 'node3', 'node4'], 'volumeId', undefined);
222
223
  expect(cryptoService.decryptNode).toHaveBeenCalledTimes(4);
223
224
  expect(cache.setNode).toHaveBeenCalledTimes(4);
224
225
  expect(cryptoCache.setNodeKeys).toHaveBeenCalledTimes(4);
@@ -320,7 +321,7 @@ describe('nodesAccess', () => {
320
321
  const result = await Array.fromAsync(access.iterateTrashedNodes());
321
322
  expect(result).toMatchObject([node1, node2, node3, node4]);
322
323
  expect(apiService.iterateTrashedNodeUids).toHaveBeenCalledWith(volumeId, undefined);
323
- expect(apiService.iterateNodes).toHaveBeenCalledWith(['node1', 'node2', 'node3', 'node4'], undefined);
324
+ expect(apiService.iterateNodes).toHaveBeenCalledWith(['node1', 'node2', 'node3', 'node4'], volumeId, undefined);
324
325
  expect(cryptoService.decryptNode).toHaveBeenCalledTimes(4);
325
326
  expect(cache.setNode).toHaveBeenCalledTimes(4);
326
327
  expect(cryptoCache.setNodeKeys).toHaveBeenCalledTimes(4);
@@ -370,7 +371,7 @@ describe('nodesAccess', () => {
370
371
 
371
372
  const result = await Array.fromAsync(access.iterateNodes(['node1', 'node2', 'node3', 'node4']));
372
373
  expect(result).toMatchObject([node1, node4, node2, node3]);
373
- expect(apiService.iterateNodes).toHaveBeenCalledWith(['node2', 'node3'], undefined);
374
+ expect(apiService.iterateNodes).toHaveBeenCalledWith(['node2', 'node3'], 'volumeId', undefined);
374
375
  });
375
376
 
376
377
  it('should remove from cache if missing on API and return back to caller', async () => {