@protontech/drive-sdk 0.9.8 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/dist/crypto/driveCrypto.d.ts +15 -15
  2. package/dist/crypto/driveCrypto.js.map +1 -1
  3. package/dist/crypto/hmac.d.ts +3 -3
  4. package/dist/crypto/hmac.js.map +1 -1
  5. package/dist/crypto/interface.d.ts +45 -25
  6. package/dist/crypto/interface.js.map +1 -1
  7. package/dist/crypto/openPGPCrypto.d.ts +37 -37
  8. package/dist/crypto/openPGPCrypto.js.map +1 -1
  9. package/dist/crypto/utils.d.ts +1 -1
  10. package/dist/interface/index.d.ts +3 -3
  11. package/dist/interface/index.js.map +1 -1
  12. package/dist/interface/nodes.d.ts +8 -0
  13. package/dist/interface/photos.d.ts +18 -1
  14. package/dist/interface/sharing.d.ts +2 -0
  15. package/dist/interface/telemetry.d.ts +1 -0
  16. package/dist/interface/telemetry.js.map +1 -1
  17. package/dist/interface/thumbnail.d.ts +2 -2
  18. package/dist/internal/apiService/apiService.js +25 -12
  19. package/dist/internal/apiService/apiService.js.map +1 -1
  20. package/dist/internal/apiService/apiService.test.js +33 -5
  21. package/dist/internal/apiService/apiService.test.js.map +1 -1
  22. package/dist/internal/apiService/driveTypes.d.ts +2942 -3187
  23. package/dist/internal/apiService/errors.test.js +17 -7
  24. package/dist/internal/apiService/errors.test.js.map +1 -1
  25. package/dist/internal/devices/manager.d.ts +1 -0
  26. package/dist/internal/devices/manager.js +11 -0
  27. package/dist/internal/devices/manager.js.map +1 -1
  28. package/dist/internal/download/apiService.d.ts +1 -1
  29. package/dist/internal/download/cryptoService.d.ts +4 -4
  30. package/dist/internal/download/cryptoService.js.map +1 -1
  31. package/dist/internal/download/fileDownloader.js.map +1 -1
  32. package/dist/internal/download/fileDownloader.test.js.map +1 -1
  33. package/dist/internal/download/thumbnailDownloader.js.map +1 -1
  34. package/dist/internal/nodes/cryptoService.d.ts +4 -4
  35. package/dist/internal/nodes/cryptoService.js +5 -3
  36. package/dist/internal/nodes/cryptoService.js.map +1 -1
  37. package/dist/internal/nodes/cryptoService.test.js.map +1 -1
  38. package/dist/internal/nodes/interface.d.ts +1 -1
  39. package/dist/internal/nodes/nodesManagement.js +0 -1
  40. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  41. package/dist/internal/photos/addToAlbum.d.ts +46 -0
  42. package/dist/internal/photos/addToAlbum.js +257 -0
  43. package/dist/internal/photos/addToAlbum.js.map +1 -0
  44. package/dist/internal/photos/addToAlbum.test.d.ts +1 -0
  45. package/dist/internal/photos/addToAlbum.test.js +409 -0
  46. package/dist/internal/photos/addToAlbum.test.js.map +1 -0
  47. package/dist/internal/photos/albums.d.ts +7 -2
  48. package/dist/internal/photos/albums.js +24 -1
  49. package/dist/internal/photos/albums.js.map +1 -1
  50. package/dist/internal/photos/albums.test.js +26 -1
  51. package/dist/internal/photos/albums.test.js.map +1 -1
  52. package/dist/internal/photos/albumsCrypto.d.ts +20 -3
  53. package/dist/internal/photos/albumsCrypto.js +27 -0
  54. package/dist/internal/photos/albumsCrypto.js.map +1 -1
  55. package/dist/internal/photos/apiService.d.ts +20 -0
  56. package/dist/internal/photos/apiService.js +142 -0
  57. package/dist/internal/photos/apiService.js.map +1 -1
  58. package/dist/internal/photos/apiService.test.d.ts +1 -0
  59. package/dist/internal/photos/apiService.test.js +199 -0
  60. package/dist/internal/photos/apiService.test.js.map +1 -0
  61. package/dist/internal/photos/errors.d.ts +4 -0
  62. package/dist/internal/photos/errors.js +17 -0
  63. package/dist/internal/photos/errors.js.map +1 -0
  64. package/dist/internal/photos/index.d.ts +1 -1
  65. package/dist/internal/photos/index.js +1 -1
  66. package/dist/internal/photos/index.js.map +1 -1
  67. package/dist/internal/photos/interface.d.ts +36 -1
  68. package/dist/internal/photos/interface.js +14 -0
  69. package/dist/internal/photos/interface.js.map +1 -1
  70. package/dist/internal/photos/nodes.js +32 -2
  71. package/dist/internal/photos/nodes.js.map +1 -1
  72. package/dist/internal/photos/nodes.test.js +25 -5
  73. package/dist/internal/photos/nodes.test.js.map +1 -1
  74. package/dist/internal/photos/timeline.d.ts +2 -5
  75. package/dist/internal/photos/timeline.js.map +1 -1
  76. package/dist/internal/photos/upload.d.ts +2 -2
  77. package/dist/internal/photos/upload.js.map +1 -1
  78. package/dist/internal/shares/apiService.js +1 -0
  79. package/dist/internal/shares/apiService.js.map +1 -1
  80. package/dist/internal/shares/interface.d.ts +1 -0
  81. package/dist/internal/sharing/apiService.d.ts +8 -1
  82. package/dist/internal/sharing/apiService.js +23 -1
  83. package/dist/internal/sharing/apiService.js.map +1 -1
  84. package/dist/internal/sharing/cryptoService.js +8 -4
  85. package/dist/internal/sharing/cryptoService.js.map +1 -1
  86. package/dist/internal/sharing/sharingManagement.d.ts +1 -0
  87. package/dist/internal/sharing/sharingManagement.js +15 -2
  88. package/dist/internal/sharing/sharingManagement.js.map +1 -1
  89. package/dist/internal/sharing/sharingManagement.test.js +30 -5
  90. package/dist/internal/sharing/sharingManagement.test.js.map +1 -1
  91. package/dist/internal/sharingPublic/nodes.d.ts +2 -2
  92. package/dist/internal/sharingPublic/nodes.js.map +1 -1
  93. package/dist/internal/upload/apiService.d.ts +5 -5
  94. package/dist/internal/upload/apiService.js.map +1 -1
  95. package/dist/internal/upload/blockVerifier.d.ts +2 -2
  96. package/dist/internal/upload/blockVerifier.js.map +1 -1
  97. package/dist/internal/upload/chunkStreamReader.d.ts +2 -2
  98. package/dist/internal/upload/chunkStreamReader.js.map +1 -1
  99. package/dist/internal/upload/chunkStreamReader.test.js.map +1 -1
  100. package/dist/internal/upload/cryptoService.d.ts +7 -7
  101. package/dist/internal/upload/cryptoService.js.map +1 -1
  102. package/dist/internal/upload/interface.d.ts +6 -6
  103. package/dist/internal/upload/manager.d.ts +1 -1
  104. package/dist/internal/upload/manager.js.map +1 -1
  105. package/dist/internal/upload/streamUploader.d.ts +1 -1
  106. package/dist/internal/utils.d.ts +1 -1
  107. package/dist/protonDriveClient.d.ts +8 -0
  108. package/dist/protonDriveClient.js +11 -0
  109. package/dist/protonDriveClient.js.map +1 -1
  110. package/dist/protonDrivePhotosClient.d.ts +42 -7
  111. package/dist/protonDrivePhotosClient.js +50 -2
  112. package/dist/protonDrivePhotosClient.js.map +1 -1
  113. package/dist/protonDrivePublicLinkClient.d.ts +9 -0
  114. package/dist/protonDrivePublicLinkClient.js +12 -0
  115. package/dist/protonDrivePublicLinkClient.js.map +1 -1
  116. package/dist/transformers.js +2 -0
  117. package/dist/transformers.js.map +1 -1
  118. package/package.json +4 -4
  119. package/src/crypto/driveCrypto.ts +15 -15
  120. package/src/crypto/hmac.ts +4 -4
  121. package/src/crypto/interface.ts +58 -27
  122. package/src/crypto/openPGPCrypto.ts +26 -26
  123. package/src/interface/index.ts +10 -2
  124. package/src/interface/nodes.ts +1 -0
  125. package/src/interface/photos.ts +19 -1
  126. package/src/interface/sharing.ts +2 -0
  127. package/src/interface/telemetry.ts +1 -0
  128. package/src/interface/thumbnail.ts +2 -2
  129. package/src/internal/apiService/apiService.test.ts +38 -6
  130. package/src/internal/apiService/apiService.ts +33 -12
  131. package/src/internal/apiService/driveTypes.ts +2942 -3187
  132. package/src/internal/devices/manager.ts +14 -0
  133. package/src/internal/download/apiService.ts +1 -1
  134. package/src/internal/download/cryptoService.ts +4 -4
  135. package/src/internal/download/fileDownloader.test.ts +4 -4
  136. package/src/internal/download/fileDownloader.ts +6 -6
  137. package/src/internal/download/thumbnailDownloader.ts +4 -4
  138. package/src/internal/nodes/cryptoService.test.ts +2 -2
  139. package/src/internal/nodes/cryptoService.ts +11 -8
  140. package/src/internal/nodes/interface.ts +1 -1
  141. package/src/internal/nodes/nodesManagement.ts +0 -1
  142. package/src/internal/photos/addToAlbum.test.ts +515 -0
  143. package/src/internal/photos/addToAlbum.ts +341 -0
  144. package/src/internal/photos/albums.test.ts +46 -22
  145. package/src/internal/photos/albums.ts +48 -2
  146. package/src/internal/photos/albumsCrypto.ts +54 -3
  147. package/src/internal/photos/apiService.test.ts +233 -0
  148. package/src/internal/photos/apiService.ts +234 -15
  149. package/src/internal/photos/errors.ts +11 -0
  150. package/src/internal/photos/index.ts +2 -2
  151. package/src/internal/photos/interface.ts +40 -1
  152. package/src/internal/photos/nodes.test.ts +27 -6
  153. package/src/internal/photos/nodes.ts +34 -2
  154. package/src/internal/photos/timeline.ts +2 -5
  155. package/src/internal/photos/upload.ts +2 -2
  156. package/src/internal/shares/apiService.ts +1 -0
  157. package/src/internal/shares/interface.ts +1 -0
  158. package/src/internal/sharing/apiService.ts +49 -5
  159. package/src/internal/sharing/cryptoService.ts +10 -4
  160. package/src/internal/sharing/sharingManagement.test.ts +33 -5
  161. package/src/internal/sharing/sharingManagement.ts +28 -6
  162. package/src/internal/sharingPublic/nodes.ts +1 -1
  163. package/src/internal/upload/apiService.ts +5 -5
  164. package/src/internal/upload/blockVerifier.ts +3 -3
  165. package/src/internal/upload/chunkStreamReader.test.ts +7 -7
  166. package/src/internal/upload/chunkStreamReader.ts +3 -3
  167. package/src/internal/upload/cryptoService.ts +9 -9
  168. package/src/internal/upload/interface.ts +6 -6
  169. package/src/internal/upload/manager.ts +2 -2
  170. package/src/internal/upload/streamUploader.ts +1 -1
  171. package/src/protonDriveClient.ts +15 -3
  172. package/src/protonDrivePhotosClient.ts +78 -22
  173. package/src/protonDrivePublicLinkClient.ts +13 -0
  174. package/src/transformers.ts +2 -0
@@ -44,6 +44,10 @@ function generateAPIAlbumNode(linkOverrides = {}, overrides = {}) {
44
44
  ...node,
45
45
  Link: { ...node.Link, Type: 3, ...linkOverrides },
46
46
  Photo: null,
47
+ Album: {
48
+ PhotoCount: 1,
49
+ CoverLinkID: 'coverLinkId',
50
+ },
47
51
  Folder: null,
48
52
  ...overrides,
49
53
  };
@@ -103,6 +107,8 @@ describe('PhotosNodesAPIService', () => {
103
107
  const nodes = await Array.fromAsync(api.iterateNodes(['volumeId~nodeId'], 'volumeId'));
104
108
  expect(nodes).toHaveLength(1);
105
109
  expect(nodes[0].type).toBe(expectedType);
110
+
111
+ return nodes;
106
112
  }
107
113
 
108
114
  it('should convert folder (type 1) to folder node', async () => {
@@ -110,16 +116,16 @@ describe('PhotosNodesAPIService', () => {
110
116
  });
111
117
 
112
118
  it('should convert album (type 3) to album node', async () => {
113
- await testIterateNodes(generateAPIAlbumNode(), NodeType.Album);
119
+ const nodes = await testIterateNodes(generateAPIAlbumNode(), NodeType.Album);
120
+
121
+ expect(nodes[0].album).toBeDefined();
122
+ expect(nodes[0].album?.photoCount).toEqual(1);
123
+ expect(nodes[0].album?.coverPhotoNodeUid).toBe('volumeId~coverLinkId');
114
124
  });
115
125
 
116
126
  it('should convert photo (type 2) to photo node with photo attributes', async () => {
117
- apiMock.post = jest.fn().mockResolvedValue({ Links: [generateAPIPhotoNode()] });
118
-
119
- const nodes = await Array.fromAsync(api.iterateNodes(['volumeId~nodeId'], 'volumeId'));
127
+ const nodes = await testIterateNodes(generateAPIPhotoNode(), NodeType.Photo);
120
128
 
121
- expect(nodes).toHaveLength(1);
122
- expect(nodes[0].type).toBe(NodeType.Photo);
123
129
  expect(nodes[0].photo).toBeDefined();
124
130
  expect(nodes[0].photo?.captureTime).toEqual(new Date(1700000000 * 1000));
125
131
  expect(nodes[0].photo?.tags).toEqual([1, 2]);
@@ -161,6 +167,10 @@ describe('PhotosNodesCache', () => {
161
167
  },
162
168
  ],
163
169
  },
170
+ album: {
171
+ photoCount: 1,
172
+ coverPhotoNodeUid: 'volumeId~coverLinkId',
173
+ },
164
174
  });
165
175
 
166
176
  const node = cache.deserialiseNode(serialisedNode);
@@ -170,6 +180,9 @@ describe('PhotosNodesCache', () => {
170
180
  expect(node.photo?.captureTime).toEqual(new Date('2023-11-14T22:13:20.000Z'));
171
181
  expect(node.photo?.albums[0].additionTime).toBeInstanceOf(Date);
172
182
  expect(node.photo?.albums[0].additionTime).toEqual(new Date('2023-11-15T10:00:00.000Z'));
183
+ expect(node.album).toBeDefined();
184
+ expect(node.album?.photoCount).toEqual(1);
185
+ expect(node.album?.coverPhotoNodeUid).toBe('volumeId~coverLinkId');
173
186
  });
174
187
 
175
188
  it('should handle node without photo attributes', () => {
@@ -187,6 +200,7 @@ describe('PhotosNodesCache', () => {
187
200
  const node = cache.deserialiseNode(serialisedNode);
188
201
 
189
202
  expect(node.photo).toBeUndefined();
203
+ expect(node.album).toBeUndefined();
190
204
  });
191
205
  });
192
206
  });
@@ -244,6 +258,10 @@ describe('PhotosNodesAccess', () => {
244
258
  tags: [1, 2],
245
259
  albums: [],
246
260
  },
261
+ album: {
262
+ photoCount: 1,
263
+ coverPhotoNodeUid: 'volumeId~coverLinkId',
264
+ },
247
265
  };
248
266
 
249
267
  // @ts-expect-error Accessing protected method for testing
@@ -253,6 +271,9 @@ describe('PhotosNodesAccess', () => {
253
271
  expect(parsedNode.photo).toBeDefined();
254
272
  expect(parsedNode.photo?.captureTime).toEqual(new Date('2023-11-14T22:13:20.000Z'));
255
273
  expect(parsedNode.photo?.tags).toEqual([1, 2]);
274
+ expect(parsedNode.album).toBeDefined();
275
+ expect(parsedNode.album?.photoCount).toEqual(1);
276
+ expect(parsedNode.album?.coverPhotoNodeUid).toBe('volumeId~coverLinkId');
256
277
  });
257
278
  });
258
279
  });
@@ -75,11 +75,21 @@ export class PhotosNodesAPIService extends NodeAPIServiceBase<
75
75
  };
76
76
  }
77
77
 
78
- if (link.Link.Type === 3) {
78
+ if (link.Link.Type === 3 && link.Album) {
79
79
  return {
80
80
  ...baseNodeMetadata,
81
+ album: {
82
+ photoCount: link.Album.PhotoCount,
83
+ coverPhotoNodeUid: link.Album.CoverLinkID
84
+ ? makeNodeUid(volumeId, link.Album.CoverLinkID)
85
+ : undefined,
86
+ },
81
87
  encryptedCrypto: {
82
88
  ...baseCryptoNodeMetadata,
89
+ folder: {
90
+ armoredExtendedAttributes: link.Album.XAttr || undefined,
91
+ armoredHashKey: link.Album.NodeHashKey as string,
92
+ },
83
93
  },
84
94
  };
85
95
  }
@@ -111,7 +121,8 @@ export class PhotosNodesCache extends NodesCacheBase<DecryptedPhotoNode> {
111
121
  typeof node !== 'object' ||
112
122
  (typeof node.photo !== 'object' && node.photo !== undefined) ||
113
123
  (typeof node.photo?.captureTime !== 'string' && node.folder?.captureTime !== undefined) ||
114
- (typeof node.photo?.albums !== 'object' && node.photo?.albums !== undefined)
124
+ (typeof node.photo?.albums !== 'object' && node.photo?.albums !== undefined) ||
125
+ (typeof node.album !== 'object' && node.album !== undefined)
115
126
  ) {
116
127
  throw new Error(`Invalid node data: ${nodeData}`);
117
128
  }
@@ -190,6 +201,18 @@ export class PhotosNodesAccess extends NodesAccessBase<EncryptedPhotoNode, Decry
190
201
  };
191
202
  }
192
203
 
204
+ if (unparsedNode.type === NodeType.Album) {
205
+ const node = parseNodeBase(this.logger, {
206
+ ...unparsedNode,
207
+ type: NodeType.Folder,
208
+ });
209
+ return {
210
+ ...node,
211
+ album: unparsedNode.album,
212
+ type: NodeType.Album,
213
+ };
214
+ }
215
+
193
216
  return parseNodeBase(this.logger, unparsedNode);
194
217
  }
195
218
  }
@@ -210,6 +233,15 @@ export class PhotosNodesCryptoService extends NodesCryptoService {
210
233
  };
211
234
  }
212
235
 
236
+ if (decryptedNode.node.type === NodeType.Album) {
237
+ return {
238
+ node: {
239
+ ...decryptedNode.node,
240
+ album: encryptedNode.album,
241
+ },
242
+ };
243
+ }
244
+
213
245
  return decryptedNode;
214
246
  }
215
247
  }
@@ -2,6 +2,7 @@ import { DriveCrypto } from '../../crypto';
2
2
  import { Logger } from '../../interface';
3
3
  import { makeNodeUid } from '../uids';
4
4
  import { PhotosAPIService } from './apiService';
5
+ import { TimelineItem } from './interface';
5
6
  import { PhotosNodesAccess } from './nodes';
6
7
  import { PhotoSharesManager } from './shares';
7
8
 
@@ -23,11 +24,7 @@ export class PhotosTimeline {
23
24
  this.nodesService = nodesService;
24
25
  }
25
26
 
26
- async *iterateTimeline(signal?: AbortSignal): AsyncGenerator<{
27
- nodeUid: string;
28
- captureTime: Date;
29
- tags: number[];
30
- }> {
27
+ async *iterateTimeline(signal?: AbortSignal): AsyncGenerator<TimelineItem> {
31
28
  const { volumeId } = await this.photoShares.getRootIDs();
32
29
  yield* this.apiService.iterateTimeline(volumeId, signal);
33
30
  }
@@ -141,7 +141,7 @@ export class PhotoUploadManager extends UploadManager {
141
141
 
142
142
  async commitDraftPhoto(
143
143
  nodeRevisionDraft: NodeRevisionDraft,
144
- manifest: Uint8Array,
144
+ manifest: Uint8Array<ArrayBuffer>,
145
145
  extendedAttributes: {
146
146
  modificationTime?: Date;
147
147
  size: number;
@@ -185,7 +185,7 @@ export class PhotoUploadCryptoService extends UploadCryptoService {
185
185
  super(driveCrypto, nodesService);
186
186
  }
187
187
 
188
- async generateContentHash(sha1: string, parentHashKey: Uint8Array): Promise<string> {
188
+ async generateContentHash(sha1: string, parentHashKey: Uint8Array<ArrayBuffer>): Promise<string> {
189
189
  return this.driveCrypto.generateLookupHash(sha1, parentHashKey);
190
190
  }
191
191
  }
@@ -170,6 +170,7 @@ function convertSharePayload(response: GetShareResponse): EncryptedShare {
170
170
  }
171
171
  : undefined,
172
172
  type: convertShareTypeNumberToEnum(response.Type),
173
+ editorsCanShare: response.EditorsCanShare
173
174
  };
174
175
  }
175
176
 
@@ -78,6 +78,7 @@ export interface EncryptedShare extends BaseShare {
78
78
  creatorEmail: string;
79
79
  encryptedCrypto: EncryptedShareCrypto;
80
80
  membership?: ShareMembership;
81
+ editorsCanShare: boolean;
81
82
  }
82
83
 
83
84
  interface ShareMembership {
@@ -36,6 +36,9 @@ type GetSharedNodesResponse =
36
36
  type GetSharedWithMeNodesResponse =
37
37
  drivePaths['/drive/v2/sharedwithme']['get']['responses']['200']['content']['application/json'];
38
38
 
39
+ type GetSharedAlbumsResponse =
40
+ drivePaths['/drive/photos/albums/shared-with-me']['get']['responses']['200']['content']['application/json'];
41
+
39
42
  type GetInvitationsResponse =
40
43
  drivePaths['/drive/v2/shares/invitations']['get']['responses']['200']['content']['application/json'];
41
44
 
@@ -61,8 +64,10 @@ type GetShareExternalInvitations =
61
64
  type GetShareMembers =
62
65
  drivePaths['/drive/v2/shares/{shareID}/members']['get']['responses']['200']['content']['application/json'];
63
66
 
64
- type PostSharedBookmarksRequest =
65
- Extract<drivePaths['/drive/v2/urls/{token}/bookmark']['post']['requestBody'], {content: object}>['content']['application/json'];
67
+ type PostSharedBookmarksRequest = Extract<
68
+ drivePaths['/drive/v2/urls/{token}/bookmark']['post']['requestBody'],
69
+ { content: object }
70
+ >['content']['application/json'];
66
71
  type PostSharedBookmarksResponse =
67
72
  drivePaths['/drive/v2/urls/{token}/bookmark']['post']['responses']['200']['content']['application/json'];
68
73
 
@@ -73,6 +78,14 @@ type PostCreateShareRequest = Extract<
73
78
  type PostCreateShareResponse =
74
79
  drivePaths['/drive/volumes/{volumeID}/shares']['post']['responses']['200']['content']['application/json'];
75
80
 
81
+ type PostChangeSharePropertiesRequest = Extract<
82
+ drivePaths['/drive/shares/{shareID}/property']['post']['requestBody'],
83
+ { content: object }
84
+ >['content']['application/json'];
85
+
86
+ type PostChangeSharePropertiesResponse =
87
+ drivePaths['/drive/shares/{shareID}/property']['post']['responses']['200']['content']['application/json'];
88
+
76
89
  type PostInviteProtonUserRequest = Extract<
77
90
  drivePaths['/drive/v2/shares/{shareID}/invitations']['post']['requestBody'],
78
91
  { content: object }
@@ -184,6 +197,30 @@ export class SharingAPIService {
184
197
  }
185
198
  anchor = response.AnchorID;
186
199
  }
200
+
201
+ if (this.shareTargetTypes.includes(ShareTargetType.Album)) {
202
+ yield* this.iterateSharedAlbumUids(signal);
203
+ }
204
+ }
205
+
206
+ // TODO: Sharing cannot know about albums. We should remove this and use
207
+ // ShareTargetTypes when it is supported by the API.
208
+ private async *iterateSharedAlbumUids(signal?: AbortSignal): AsyncGenerator<string> {
209
+ let anchor = '';
210
+ while (true) {
211
+ const response = await this.apiService.get<GetSharedAlbumsResponse>(
212
+ `drive/photos/albums/shared-with-me?${anchor ? `AnchorID=${anchor}` : ''}`,
213
+ signal,
214
+ );
215
+ for (const album of response.Albums) {
216
+ yield makeNodeUid(album.VolumeID, album.LinkID);
217
+ }
218
+
219
+ if (!response.More || !response.AnchorID) {
220
+ break;
221
+ }
222
+ anchor = response.AnchorID;
223
+ }
187
224
  }
188
225
 
189
226
  async *iterateInvitationUids(signal?: AbortSignal): AsyncGenerator<string> {
@@ -304,7 +341,7 @@ export class SharingAPIService {
304
341
  addressId: string;
305
342
  addressKeyId: string;
306
343
  }): Promise<void> {
307
- await this.apiService.post<PostSharedBookmarksRequest, PostSharedBookmarksResponse>(
344
+ await this.apiService.post<PostSharedBookmarksRequest, PostSharedBookmarksResponse>(
308
345
  `drive/v2/urls/${bookmark.token}/bookmark`,
309
346
  {
310
347
  BookmarkShareURL: {
@@ -363,7 +400,7 @@ export class SharingAPIService {
363
400
  base64PassphraseKeyPacket: string;
364
401
  base64NameKeyPacket: string;
365
402
  },
366
- ): Promise<string> {
403
+ ): Promise<{ shareId: string; editorsCanShare: boolean }> {
367
404
  const { volumeId, nodeId } = splitNodeUid(nodeUid);
368
405
  const response = await this.apiService.post<PostCreateShareRequest, PostCreateShareResponse>(
369
406
  `drive/volumes/${volumeId}/shares`,
@@ -378,13 +415,20 @@ export class SharingAPIService {
378
415
  NameKeyPacket: node.base64NameKeyPacket,
379
416
  },
380
417
  );
381
- return response.Share.ID;
418
+ return { shareId: response.Share.ID, editorsCanShare: response.Share.EditorsCanShare };
382
419
  }
383
420
 
384
421
  async deleteShare(shareId: string, force: boolean = false): Promise<void> {
385
422
  await this.apiService.delete(`drive/shares/${shareId}?Force=${force ? 1 : 0}`);
386
423
  }
387
424
 
425
+ async changeShareProperties(shareId: string, { editorsCanShare }: { editorsCanShare: boolean }) {
426
+ await this.apiService.post<PostChangeSharePropertiesRequest, PostChangeSharePropertiesResponse>(
427
+ `drive/shares/${shareId}/property`,
428
+ { EditorsCanShare: editorsCanShare },
429
+ );
430
+ }
431
+
388
432
  async inviteProtonUser(
389
433
  shareId: string,
390
434
  invitation: EncryptedInvitationRequest,
@@ -213,7 +213,7 @@ export class SharingCryptoService {
213
213
  } catch (error: unknown) {
214
214
  const message = getErrorMessage(error);
215
215
  const errorMessage = c('Error').t`Failed to decrypt item name: ${message}`;
216
- nodeName = resultError(new Error(errorMessage));
216
+ nodeName = resultError(new Error(errorMessage, { cause: error }));
217
217
  }
218
218
 
219
219
  return {
@@ -458,7 +458,10 @@ export class SharingCryptoService {
458
458
  urlPassword = result.password;
459
459
  customPassword = resultOk(result.customPassword);
460
460
  } catch (originalError: unknown) {
461
- const error = originalError instanceof Error ? originalError : new Error(c('Error').t`Unknown error`);
461
+ const error =
462
+ originalError instanceof Error
463
+ ? originalError
464
+ : new Error(c('Error').t`Unknown error`, { cause: originalError });
462
465
  return {
463
466
  url: resultError(error),
464
467
  customPassword: resultError(error),
@@ -473,7 +476,10 @@ export class SharingCryptoService {
473
476
  try {
474
477
  shareKey = await this.decryptBookmarkKey(encryptedBookmark, password);
475
478
  } catch (originalError: unknown) {
476
- const error = originalError instanceof Error ? originalError : new Error(c('Error').t`Unknown error`);
479
+ const error =
480
+ originalError instanceof Error
481
+ ? originalError
482
+ : new Error(c('Error').t`Unknown error`, { cause: originalError });
477
483
  return {
478
484
  url,
479
485
  customPassword,
@@ -577,7 +583,7 @@ export class SharingCryptoService {
577
583
 
578
584
  const message = getErrorMessage(error);
579
585
  const errorMessage = c('Error').t`Failed to decrypt bookmark name: ${message}`;
580
- return resultError(new Error(errorMessage));
586
+ return resultError(new Error(errorMessage, { cause: error }));
581
587
  }
582
588
  }
583
589
  }
@@ -18,6 +18,8 @@ import { SharingManagement } from './sharingManagement';
18
18
  import { ValidationError } from '../../errors';
19
19
  import { ErrorCode } from '../apiService';
20
20
 
21
+ const DEFAULT_SHARE_ID = 'shareId';
22
+
21
23
  describe('SharingManagement', () => {
22
24
  let logger: Logger;
23
25
  let apiService: SharingAPIService;
@@ -34,7 +36,7 @@ describe('SharingManagement', () => {
34
36
 
35
37
  // @ts-expect-error No need to implement all methods for mocking
36
38
  apiService = {
37
- createStandardShare: jest.fn().mockReturnValue('newShareId'),
39
+ createStandardShare: jest.fn().mockReturnValue({ shareId: 'newShareId', editorsCanShare: false }),
38
40
  getShareInvitations: jest.fn().mockResolvedValue([]),
39
41
  getShareExternalInvitations: jest.fn().mockResolvedValue([]),
40
42
  getShareMembers: jest.fn().mockResolvedValue([]),
@@ -63,6 +65,7 @@ describe('SharingManagement', () => {
63
65
  publicUrl: 'publicLinkUrl',
64
66
  }),
65
67
  updatePublicLink: jest.fn(),
68
+ changeShareProperties: jest.fn(),
66
69
  };
67
70
  // @ts-expect-error No need to implement all methods for mocking
68
71
  cache = {
@@ -98,7 +101,7 @@ describe('SharingManagement', () => {
98
101
  // @ts-expect-error No need to implement all methods for mocking
99
102
  sharesService = {
100
103
  loadEncryptedShare: jest.fn().mockResolvedValue({
101
- id: 'shareId',
104
+ id: DEFAULT_SHARE_ID,
102
105
  addressId: 'addressId',
103
106
  creatorEmail: 'address@example.com',
104
107
  passphraseSessionKey: 'sharePassphraseSessionKey',
@@ -106,9 +109,11 @@ describe('SharingManagement', () => {
106
109
  };
107
110
  // @ts-expect-error No need to implement all methods for mocking
108
111
  nodesService = {
109
- getNode: jest
110
- .fn()
111
- .mockImplementation((nodeUid) => ({ nodeUid, shareId: 'shareId', name: { ok: true, value: 'name' } })),
112
+ getNode: jest.fn().mockImplementation((nodeUid) => ({
113
+ nodeUid,
114
+ shareId: DEFAULT_SHARE_ID,
115
+ name: { ok: true, value: 'name' },
116
+ })),
112
117
  getNodeKeys: jest.fn().mockImplementation((nodeUid) => ({ key: 'node-key' })),
113
118
  getNodePrivateAndSessionKeys: jest.fn().mockImplementation((nodeUid) => ({})),
114
119
  getRootNodeEmailKey: jest.fn().mockResolvedValue({ email: 'volume-email', addressKey: 'volume-key' }),
@@ -225,6 +230,7 @@ describe('SharingManagement', () => {
225
230
  nonProtonInvitations: [],
226
231
  members: [],
227
232
  publicLink: undefined,
233
+ editorsCanShare: false,
228
234
  });
229
235
  expect(apiService.updateInvitation).not.toHaveBeenCalled();
230
236
  expect(apiService.inviteProtonUser).toHaveBeenCalled();
@@ -395,6 +401,28 @@ describe('SharingManagement', () => {
395
401
  expect(cache.addSharedByMeNodeUid).not.toHaveBeenCalled();
396
402
  });
397
403
 
404
+ it('should update editorsCanChange', async () => {
405
+ const sharingInfo = await sharingManagement.shareNode(nodeUid, {
406
+ editorsCanShare: true,
407
+ });
408
+
409
+ expect(sharingInfo).toEqual({
410
+ protonInvitations: [
411
+ {
412
+ ...invitation,
413
+ role: 'viewer',
414
+ },
415
+ ],
416
+ nonProtonInvitations: [externalInvitation],
417
+ members: [member],
418
+ publicLink: undefined,
419
+ editorsCanShare: true,
420
+ });
421
+ expect(apiService.changeShareProperties).toHaveBeenCalledWith(DEFAULT_SHARE_ID, {
422
+ editorsCanShare: true,
423
+ });
424
+ });
425
+
398
426
  it('should be no-op if no change', async () => {
399
427
  const sharingInfo = await sharingManagement.shareNode(nodeUid, {
400
428
  users: [{ email: 'internal-email', role: MemberRole.Viewer }],
@@ -77,11 +77,12 @@ export class SharingManagement {
77
77
  return;
78
78
  }
79
79
 
80
- const [protonInvitations, nonProtonInvitations, members, publicLink] = await Promise.all([
80
+ const [protonInvitations, nonProtonInvitations, members, publicLink, share] = await Promise.all([
81
81
  Array.fromAsync(this.iterateShareInvitations(node.shareId)),
82
82
  Array.fromAsync(this.iterateShareExternalInvitations(node.shareId)),
83
83
  Array.fromAsync(this.iterateShareMembers(node.shareId)),
84
84
  this.getPublicLink(node.shareId),
85
+ this.sharesService.loadEncryptedShare(node.shareId),
85
86
  ]);
86
87
 
87
88
  return {
@@ -89,6 +90,7 @@ export class SharingManagement {
89
90
  nonProtonInvitations,
90
91
  members,
91
92
  publicLink,
93
+ editorsCanShare: share.editorsCanShare,
92
94
  };
93
95
  }
94
96
 
@@ -161,6 +163,7 @@ export class SharingManagement {
161
163
  nonProtonInvitations: [],
162
164
  members: [],
163
165
  publicLink: undefined,
166
+ editorsCanShare: result.editorsCanShare,
164
167
  };
165
168
  contextShareAddress = result.contextShareAddress;
166
169
  } catch (error: unknown) {
@@ -184,6 +187,11 @@ export class SharingManagement {
184
187
  contextShareAddress = await this.nodesService.getRootNodeEmailKey(nodeUid);
185
188
  }
186
189
 
190
+ if (settings.editorsCanShare !== undefined) {
191
+ await this.setEditorsCanShare(currentSharing.share.shareId, settings.editorsCanShare);
192
+ currentSharing.editorsCanShare = settings.editorsCanShare;
193
+ }
194
+
187
195
  const emailOptions: EmailOptions = {
188
196
  message: settings.emailOptions?.message,
189
197
  nodeName: settings.emailOptions?.includeNodeName ? currentSharing.nodeName : undefined,
@@ -294,6 +302,7 @@ export class SharingManagement {
294
302
  nonProtonInvitations: currentSharing.nonProtonInvitations,
295
303
  members: currentSharing.members,
296
304
  publicLink: currentSharing.publicLink,
305
+ editorsCanShare: currentSharing.editorsCanShare,
297
306
  };
298
307
  }
299
308
 
@@ -385,6 +394,7 @@ export class SharingManagement {
385
394
  nonProtonInvitations: currentSharing.nonProtonInvitations,
386
395
  members: currentSharing.members,
387
396
  publicLink: currentSharing.publicLink,
397
+ editorsCanShare: currentSharing.editorsCanShare,
388
398
  };
389
399
  }
390
400
 
@@ -415,7 +425,9 @@ export class SharingManagement {
415
425
  };
416
426
  }
417
427
 
418
- private async createShare(nodeUid: string): Promise<{ share: Share; contextShareAddress: ContextShareAddress }> {
428
+ private async createShare(
429
+ nodeUid: string,
430
+ ): Promise<{ share: Share; contextShareAddress: ContextShareAddress; editorsCanShare: boolean }> {
419
431
  const node = await this.nodesService.getNode(nodeUid);
420
432
  if (!node.parentUid) {
421
433
  throw new ValidationError(c('Error').t`Cannot share root folder`);
@@ -426,10 +438,15 @@ export class SharingManagement {
426
438
 
427
439
  const nodeKeys = await this.nodesService.getNodePrivateAndSessionKeys(nodeUid);
428
440
  const keys = await this.cryptoService.generateShareKeys(nodeKeys, addressKey);
429
- const shareId = await this.apiService.createStandardShare(nodeUid, addressId, keys.shareKey.encrypted, {
430
- base64PassphraseKeyPacket: keys.base64PpassphraseKeyPacket,
431
- base64NameKeyPacket: keys.base64NameKeyPacket,
432
- });
441
+ const { shareId, editorsCanShare } = await this.apiService.createStandardShare(
442
+ nodeUid,
443
+ addressId,
444
+ keys.shareKey.encrypted,
445
+ {
446
+ base64PassphraseKeyPacket: keys.base64PpassphraseKeyPacket,
447
+ base64NameKeyPacket: keys.base64NameKeyPacket,
448
+ },
449
+ );
433
450
  await this.nodesService.notifyNodeChanged(nodeUid);
434
451
  if (await this.cache.hasSharedByMeNodeUidsLoaded()) {
435
452
  await this.cache.addSharedByMeNodeUid(nodeUid);
@@ -449,9 +466,14 @@ export class SharingManagement {
449
466
  return {
450
467
  share,
451
468
  contextShareAddress,
469
+ editorsCanShare,
452
470
  };
453
471
  }
454
472
 
473
+ private async setEditorsCanShare(shareId: string, editorsCanShare: boolean) {
474
+ await this.apiService.changeShareProperties(shareId, { editorsCanShare });
475
+ }
476
+
455
477
  /**
456
478
  * Deletes the share even if it is not empty.
457
479
  */
@@ -18,7 +18,7 @@ import { SharingPublicSharesManager } from './shares';
18
18
 
19
19
  export class SharingPublicNodesCryptoService extends NodesCryptoService {
20
20
  async generateDocument(
21
- parentKeys: { key: PrivateKey; hashKey: Uint8Array },
21
+ parentKeys: { key: PrivateKey; hashKey: Uint8Array<ArrayBuffer> },
22
22
  signingKeys: NodeSigningKeys,
23
23
  name: string,
24
24
  ) {
@@ -142,7 +142,7 @@ export class UploadAPIService {
142
142
  }
143
143
 
144
144
  async getVerificationData(draftNodeRevisionUid: string): Promise<{
145
- verificationCode: Uint8Array;
145
+ verificationCode: Uint8Array<ArrayBuffer>;
146
146
  base64ContentKeyPacket: string;
147
147
  }> {
148
148
  const { volumeId, nodeId, revisionId } = splitNodeRevisionUid(draftNodeRevisionUid);
@@ -162,14 +162,14 @@ export class UploadAPIService {
162
162
  blocks: {
163
163
  contentBlocks: {
164
164
  index: number;
165
- hash: Uint8Array;
165
+ hash: Uint8Array<ArrayBuffer>;
166
166
  encryptedSize: number;
167
167
  armoredSignature: string;
168
- verificationToken: Uint8Array;
168
+ verificationToken: Uint8Array<ArrayBuffer>;
169
169
  }[];
170
170
  thumbnails?: {
171
171
  type: ThumbnailType;
172
- hash: Uint8Array;
172
+ hash: Uint8Array<ArrayBuffer>;
173
173
  encryptedSize: number;
174
174
  }[];
175
175
  },
@@ -260,7 +260,7 @@ export class UploadAPIService {
260
260
  async uploadBlock(
261
261
  url: string,
262
262
  token: string,
263
- block: Uint8Array,
263
+ block: Uint8Array<ArrayBuffer>,
264
264
  onProgress?: (uploadedBytes: number) => void,
265
265
  signal?: AbortSignal,
266
266
  ): Promise<void> {
@@ -3,7 +3,7 @@ import { UploadAPIService } from './apiService';
3
3
  import { UploadCryptoService } from './cryptoService';
4
4
 
5
5
  export class BlockVerifier {
6
- private verificationCode?: Uint8Array;
6
+ private verificationCode?: Uint8Array<ArrayBuffer>;
7
7
  private contentKeyPacketSessionKey?: SessionKey;
8
8
 
9
9
  constructor(
@@ -26,8 +26,8 @@ export class BlockVerifier {
26
26
  );
27
27
  }
28
28
 
29
- async verifyBlock(encryptedBlock: Uint8Array): Promise<{
30
- verificationToken: Uint8Array;
29
+ async verifyBlock(encryptedBlock: Uint8Array<ArrayBuffer>): Promise<{
30
+ verificationToken: Uint8Array<ArrayBuffer>;
31
31
  }> {
32
32
  if (!this.verificationCode || !this.contentKeyPacketSessionKey) {
33
33
  throw new Error('Verifying block before loading verification data');