@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.
- package/dist/crypto/driveCrypto.d.ts +15 -15
- package/dist/crypto/driveCrypto.js.map +1 -1
- package/dist/crypto/hmac.d.ts +3 -3
- package/dist/crypto/hmac.js.map +1 -1
- package/dist/crypto/interface.d.ts +45 -25
- package/dist/crypto/interface.js.map +1 -1
- package/dist/crypto/openPGPCrypto.d.ts +37 -37
- package/dist/crypto/openPGPCrypto.js.map +1 -1
- package/dist/crypto/utils.d.ts +1 -1
- package/dist/interface/index.d.ts +3 -3
- package/dist/interface/index.js.map +1 -1
- package/dist/interface/nodes.d.ts +8 -0
- package/dist/interface/photos.d.ts +18 -1
- package/dist/interface/sharing.d.ts +2 -0
- package/dist/interface/telemetry.d.ts +1 -0
- package/dist/interface/telemetry.js.map +1 -1
- package/dist/interface/thumbnail.d.ts +2 -2
- package/dist/internal/apiService/apiService.js +25 -12
- package/dist/internal/apiService/apiService.js.map +1 -1
- package/dist/internal/apiService/apiService.test.js +33 -5
- package/dist/internal/apiService/apiService.test.js.map +1 -1
- package/dist/internal/apiService/driveTypes.d.ts +2942 -3187
- package/dist/internal/apiService/errors.test.js +17 -7
- package/dist/internal/apiService/errors.test.js.map +1 -1
- package/dist/internal/devices/manager.d.ts +1 -0
- package/dist/internal/devices/manager.js +11 -0
- package/dist/internal/devices/manager.js.map +1 -1
- package/dist/internal/download/apiService.d.ts +1 -1
- package/dist/internal/download/cryptoService.d.ts +4 -4
- package/dist/internal/download/cryptoService.js.map +1 -1
- package/dist/internal/download/fileDownloader.js.map +1 -1
- package/dist/internal/download/fileDownloader.test.js.map +1 -1
- package/dist/internal/download/thumbnailDownloader.js.map +1 -1
- package/dist/internal/nodes/cryptoService.d.ts +4 -4
- package/dist/internal/nodes/cryptoService.js +5 -3
- package/dist/internal/nodes/cryptoService.js.map +1 -1
- package/dist/internal/nodes/cryptoService.test.js.map +1 -1
- package/dist/internal/nodes/interface.d.ts +1 -1
- package/dist/internal/nodes/nodesManagement.js +0 -1
- package/dist/internal/nodes/nodesManagement.js.map +1 -1
- package/dist/internal/photos/addToAlbum.d.ts +46 -0
- package/dist/internal/photos/addToAlbum.js +257 -0
- package/dist/internal/photos/addToAlbum.js.map +1 -0
- package/dist/internal/photos/addToAlbum.test.d.ts +1 -0
- package/dist/internal/photos/addToAlbum.test.js +409 -0
- package/dist/internal/photos/addToAlbum.test.js.map +1 -0
- package/dist/internal/photos/albums.d.ts +7 -2
- package/dist/internal/photos/albums.js +24 -1
- package/dist/internal/photos/albums.js.map +1 -1
- package/dist/internal/photos/albums.test.js +26 -1
- package/dist/internal/photos/albums.test.js.map +1 -1
- package/dist/internal/photos/albumsCrypto.d.ts +20 -3
- package/dist/internal/photos/albumsCrypto.js +27 -0
- package/dist/internal/photos/albumsCrypto.js.map +1 -1
- package/dist/internal/photos/apiService.d.ts +20 -0
- package/dist/internal/photos/apiService.js +142 -0
- package/dist/internal/photos/apiService.js.map +1 -1
- package/dist/internal/photos/apiService.test.d.ts +1 -0
- package/dist/internal/photos/apiService.test.js +199 -0
- package/dist/internal/photos/apiService.test.js.map +1 -0
- package/dist/internal/photos/errors.d.ts +4 -0
- package/dist/internal/photos/errors.js +17 -0
- package/dist/internal/photos/errors.js.map +1 -0
- package/dist/internal/photos/index.d.ts +1 -1
- package/dist/internal/photos/index.js +1 -1
- package/dist/internal/photos/index.js.map +1 -1
- package/dist/internal/photos/interface.d.ts +36 -1
- package/dist/internal/photos/interface.js +14 -0
- package/dist/internal/photos/interface.js.map +1 -1
- package/dist/internal/photos/nodes.js +32 -2
- package/dist/internal/photos/nodes.js.map +1 -1
- package/dist/internal/photos/nodes.test.js +25 -5
- package/dist/internal/photos/nodes.test.js.map +1 -1
- package/dist/internal/photos/timeline.d.ts +2 -5
- package/dist/internal/photos/timeline.js.map +1 -1
- package/dist/internal/photos/upload.d.ts +2 -2
- package/dist/internal/photos/upload.js.map +1 -1
- package/dist/internal/shares/apiService.js +1 -0
- package/dist/internal/shares/apiService.js.map +1 -1
- package/dist/internal/shares/interface.d.ts +1 -0
- package/dist/internal/sharing/apiService.d.ts +8 -1
- package/dist/internal/sharing/apiService.js +23 -1
- package/dist/internal/sharing/apiService.js.map +1 -1
- package/dist/internal/sharing/cryptoService.js +8 -4
- package/dist/internal/sharing/cryptoService.js.map +1 -1
- package/dist/internal/sharing/sharingManagement.d.ts +1 -0
- package/dist/internal/sharing/sharingManagement.js +15 -2
- package/dist/internal/sharing/sharingManagement.js.map +1 -1
- package/dist/internal/sharing/sharingManagement.test.js +30 -5
- package/dist/internal/sharing/sharingManagement.test.js.map +1 -1
- package/dist/internal/sharingPublic/nodes.d.ts +2 -2
- package/dist/internal/sharingPublic/nodes.js.map +1 -1
- package/dist/internal/upload/apiService.d.ts +5 -5
- package/dist/internal/upload/apiService.js.map +1 -1
- package/dist/internal/upload/blockVerifier.d.ts +2 -2
- package/dist/internal/upload/blockVerifier.js.map +1 -1
- package/dist/internal/upload/chunkStreamReader.d.ts +2 -2
- package/dist/internal/upload/chunkStreamReader.js.map +1 -1
- package/dist/internal/upload/chunkStreamReader.test.js.map +1 -1
- package/dist/internal/upload/cryptoService.d.ts +7 -7
- package/dist/internal/upload/cryptoService.js.map +1 -1
- package/dist/internal/upload/interface.d.ts +6 -6
- package/dist/internal/upload/manager.d.ts +1 -1
- package/dist/internal/upload/manager.js.map +1 -1
- package/dist/internal/upload/streamUploader.d.ts +1 -1
- package/dist/internal/utils.d.ts +1 -1
- package/dist/protonDriveClient.d.ts +8 -0
- package/dist/protonDriveClient.js +11 -0
- package/dist/protonDriveClient.js.map +1 -1
- package/dist/protonDrivePhotosClient.d.ts +42 -7
- package/dist/protonDrivePhotosClient.js +50 -2
- package/dist/protonDrivePhotosClient.js.map +1 -1
- package/dist/protonDrivePublicLinkClient.d.ts +9 -0
- package/dist/protonDrivePublicLinkClient.js +12 -0
- package/dist/protonDrivePublicLinkClient.js.map +1 -1
- package/dist/transformers.js +2 -0
- package/dist/transformers.js.map +1 -1
- package/package.json +4 -4
- package/src/crypto/driveCrypto.ts +15 -15
- package/src/crypto/hmac.ts +4 -4
- package/src/crypto/interface.ts +58 -27
- package/src/crypto/openPGPCrypto.ts +26 -26
- package/src/interface/index.ts +10 -2
- package/src/interface/nodes.ts +1 -0
- package/src/interface/photos.ts +19 -1
- package/src/interface/sharing.ts +2 -0
- package/src/interface/telemetry.ts +1 -0
- package/src/interface/thumbnail.ts +2 -2
- package/src/internal/apiService/apiService.test.ts +38 -6
- package/src/internal/apiService/apiService.ts +33 -12
- package/src/internal/apiService/driveTypes.ts +2942 -3187
- package/src/internal/devices/manager.ts +14 -0
- package/src/internal/download/apiService.ts +1 -1
- package/src/internal/download/cryptoService.ts +4 -4
- package/src/internal/download/fileDownloader.test.ts +4 -4
- package/src/internal/download/fileDownloader.ts +6 -6
- package/src/internal/download/thumbnailDownloader.ts +4 -4
- package/src/internal/nodes/cryptoService.test.ts +2 -2
- package/src/internal/nodes/cryptoService.ts +11 -8
- package/src/internal/nodes/interface.ts +1 -1
- package/src/internal/nodes/nodesManagement.ts +0 -1
- package/src/internal/photos/addToAlbum.test.ts +515 -0
- package/src/internal/photos/addToAlbum.ts +341 -0
- package/src/internal/photos/albums.test.ts +46 -22
- package/src/internal/photos/albums.ts +48 -2
- package/src/internal/photos/albumsCrypto.ts +54 -3
- package/src/internal/photos/apiService.test.ts +233 -0
- package/src/internal/photos/apiService.ts +234 -15
- package/src/internal/photos/errors.ts +11 -0
- package/src/internal/photos/index.ts +2 -2
- package/src/internal/photos/interface.ts +40 -1
- package/src/internal/photos/nodes.test.ts +27 -6
- package/src/internal/photos/nodes.ts +34 -2
- package/src/internal/photos/timeline.ts +2 -5
- package/src/internal/photos/upload.ts +2 -2
- package/src/internal/shares/apiService.ts +1 -0
- package/src/internal/shares/interface.ts +1 -0
- package/src/internal/sharing/apiService.ts +49 -5
- package/src/internal/sharing/cryptoService.ts +10 -4
- package/src/internal/sharing/sharingManagement.test.ts +33 -5
- package/src/internal/sharing/sharingManagement.ts +28 -6
- package/src/internal/sharingPublic/nodes.ts +1 -1
- package/src/internal/upload/apiService.ts +5 -5
- package/src/internal/upload/blockVerifier.ts +3 -3
- package/src/internal/upload/chunkStreamReader.test.ts +7 -7
- package/src/internal/upload/chunkStreamReader.ts +3 -3
- package/src/internal/upload/cryptoService.ts +9 -9
- package/src/internal/upload/interface.ts +6 -6
- package/src/internal/upload/manager.ts +2 -2
- package/src/internal/upload/streamUploader.ts +1 -1
- package/src/protonDriveClient.ts +15 -3
- package/src/protonDrivePhotosClient.ts +78 -22
- package/src/protonDrivePublicLinkClient.ts +13 -0
- 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
|
-
|
|
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
|
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
111
|
-
|
|
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(
|
|
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(
|
|
430
|
-
|
|
431
|
-
|
|
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');
|