@protontech/drive-sdk 0.2.0 → 0.3.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/diagnostic/interface.d.ts +26 -29
- package/dist/diagnostic/sdkDiagnostic.js +50 -24
- package/dist/diagnostic/sdkDiagnostic.js.map +1 -1
- package/dist/errors.d.ts +3 -3
- package/dist/errors.js +7 -7
- package/dist/errors.js.map +1 -1
- package/dist/interface/author.d.ts +1 -1
- package/dist/interface/events.d.ts +1 -1
- package/dist/interface/events.js.map +1 -1
- package/dist/interface/sharing.d.ts +3 -1
- package/dist/internal/apiService/apiService.js +11 -3
- package/dist/internal/apiService/apiService.js.map +1 -1
- package/dist/internal/apiService/errorCodes.d.ts +1 -0
- package/dist/internal/apiService/errors.js +1 -0
- package/dist/internal/apiService/errors.js.map +1 -1
- package/dist/internal/nodes/apiService.js +3 -0
- package/dist/internal/nodes/apiService.js.map +1 -1
- package/dist/internal/nodes/apiService.test.js +18 -0
- package/dist/internal/nodes/apiService.test.js.map +1 -1
- package/dist/internal/nodes/cache.js +3 -1
- package/dist/internal/nodes/cache.js.map +1 -1
- package/dist/internal/nodes/cache.test.js +25 -0
- package/dist/internal/nodes/cache.test.js.map +1 -1
- package/dist/internal/nodes/cryptoService.js +44 -20
- package/dist/internal/nodes/cryptoService.js.map +1 -1
- package/dist/internal/nodes/nodesAccess.js +2 -2
- package/dist/internal/nodes/nodesAccess.js.map +1 -1
- package/dist/internal/nodes/nodesManagement.js +0 -2
- package/dist/internal/nodes/nodesManagement.js.map +1 -1
- package/dist/internal/sharing/cryptoService.d.ts +1 -0
- package/dist/internal/sharing/cryptoService.js +14 -5
- package/dist/internal/sharing/cryptoService.js.map +1 -1
- package/dist/internal/sharing/interface.d.ts +0 -4
- package/dist/internal/sharing/sharingAccess.d.ts +1 -0
- package/dist/internal/sharing/sharingAccess.js +10 -3
- package/dist/internal/sharing/sharingAccess.js.map +1 -1
- package/dist/internal/sharing/sharingAccess.test.js +9 -3
- package/dist/internal/sharing/sharingAccess.test.js.map +1 -1
- package/dist/internal/sharing/sharingManagement.js +27 -16
- package/dist/internal/sharing/sharingManagement.js.map +1 -1
- package/dist/internal/sharing/sharingManagement.test.js +29 -13
- package/dist/internal/sharing/sharingManagement.test.js.map +1 -1
- package/dist/internal/upload/manager.js +1 -3
- package/dist/internal/upload/manager.js.map +1 -1
- package/dist/internal/upload/manager.test.js +2 -2
- package/dist/internal/upload/manager.test.js.map +1 -1
- package/dist/protonDriveClient.d.ts +8 -8
- package/dist/protonDriveClient.js +14 -14
- package/dist/protonDriveClient.js.map +1 -1
- package/package.json +1 -1
- package/src/diagnostic/interface.ts +27 -29
- package/src/diagnostic/sdkDiagnostic.ts +58 -30
- package/src/errors.ts +5 -5
- package/src/interface/author.ts +1 -1
- package/src/interface/events.ts +1 -7
- package/src/interface/sharing.ts +3 -1
- package/src/internal/apiService/apiService.ts +12 -3
- package/src/internal/apiService/errorCodes.ts +1 -0
- package/src/internal/apiService/errors.ts +1 -0
- package/src/internal/nodes/apiService.test.ts +28 -0
- package/src/internal/nodes/apiService.ts +3 -0
- package/src/internal/nodes/cache.test.ts +28 -0
- package/src/internal/nodes/cache.ts +3 -1
- package/src/internal/nodes/cryptoService.ts +68 -34
- package/src/internal/nodes/nodesAccess.ts +2 -2
- package/src/internal/nodes/nodesManagement.ts +0 -3
- package/src/internal/sharing/cryptoService.ts +19 -5
- package/src/internal/sharing/interface.ts +0 -4
- package/src/internal/sharing/sharingAccess.test.ts +10 -4
- package/src/internal/sharing/sharingAccess.ts +10 -2
- package/src/internal/sharing/sharingManagement.test.ts +54 -24
- package/src/internal/sharing/sharingManagement.ts +47 -16
- package/src/internal/upload/manager.test.ts +2 -2
- package/src/internal/upload/manager.ts +2 -4
- package/src/protonDriveClient.ts +17 -16
|
@@ -411,10 +411,7 @@ export class SharingCryptoService {
|
|
|
411
411
|
};
|
|
412
412
|
case PublicLinkFlags.GeneratedPasswordIncluded:
|
|
413
413
|
case PublicLinkFlags.GeneratedPasswordWithCustomPassword:
|
|
414
|
-
return
|
|
415
|
-
password: password.substring(0, PUBLIC_LINK_GENERATED_PASSWORD_LENGTH),
|
|
416
|
-
customPassword: password.substring(PUBLIC_LINK_GENERATED_PASSWORD_LENGTH) || undefined,
|
|
417
|
-
};
|
|
414
|
+
return splitGeneratedAndCustomPassword(password);
|
|
418
415
|
default:
|
|
419
416
|
throw new Error(`Unsupported public link with flags: ${encryptedPublicLink.flags}`);
|
|
420
417
|
}
|
|
@@ -422,18 +419,24 @@ export class SharingCryptoService {
|
|
|
422
419
|
|
|
423
420
|
async decryptBookmark(encryptedBookmark: EncryptedBookmark): Promise<{
|
|
424
421
|
url: Result<string, Error>;
|
|
422
|
+
customPassword: Result<string | undefined, Error>;
|
|
425
423
|
nodeName: Result<string, Error | InvalidNameError>;
|
|
426
424
|
}> {
|
|
427
425
|
// TODO: Signatures are not checked and not specified in the interface.
|
|
428
426
|
// In the future, we will need to add authorship verification.
|
|
429
427
|
|
|
430
428
|
let urlPassword: string;
|
|
429
|
+
let customPassword: Result<string | undefined, Error>;
|
|
431
430
|
try {
|
|
432
|
-
|
|
431
|
+
const password = await this.decryptBookmarkUrlPassword(encryptedBookmark);
|
|
432
|
+
const result = splitGeneratedAndCustomPassword(password);
|
|
433
|
+
urlPassword = result.password;
|
|
434
|
+
customPassword = resultOk(result.customPassword);
|
|
433
435
|
} catch (originalError: unknown) {
|
|
434
436
|
const error = originalError instanceof Error ? originalError : new Error(c('Error').t`Unknown error`);
|
|
435
437
|
return {
|
|
436
438
|
url: resultError(error),
|
|
439
|
+
customPassword: resultError(error),
|
|
437
440
|
nodeName: resultError(error),
|
|
438
441
|
};
|
|
439
442
|
}
|
|
@@ -448,6 +451,7 @@ export class SharingCryptoService {
|
|
|
448
451
|
const error = originalError instanceof Error ? originalError : new Error(c('Error').t`Unknown error`);
|
|
449
452
|
return {
|
|
450
453
|
url,
|
|
454
|
+
customPassword,
|
|
451
455
|
nodeName: resultError(error),
|
|
452
456
|
};
|
|
453
457
|
}
|
|
@@ -456,6 +460,7 @@ export class SharingCryptoService {
|
|
|
456
460
|
|
|
457
461
|
return {
|
|
458
462
|
url,
|
|
463
|
+
customPassword,
|
|
459
464
|
nodeName,
|
|
460
465
|
};
|
|
461
466
|
}
|
|
@@ -551,3 +556,12 @@ export class SharingCryptoService {
|
|
|
551
556
|
}
|
|
552
557
|
}
|
|
553
558
|
}
|
|
559
|
+
|
|
560
|
+
function splitGeneratedAndCustomPassword(concatenatedPassword: string): {
|
|
561
|
+
password: string;
|
|
562
|
+
customPassword?: string;
|
|
563
|
+
} {
|
|
564
|
+
const password = concatenatedPassword.substring(0, PUBLIC_LINK_GENERATED_PASSWORD_LENGTH);
|
|
565
|
+
const customPassword = concatenatedPassword.substring(PUBLIC_LINK_GENERATED_PASSWORD_LENGTH) || undefined;
|
|
566
|
+
return { password, customPassword };
|
|
567
|
+
}
|
|
@@ -145,10 +145,6 @@ export interface SharesService {
|
|
|
145
145
|
getMyFilesIDs(): Promise<{ volumeId: string }>;
|
|
146
146
|
loadEncryptedShare(shareId: string): Promise<EncryptedShare>;
|
|
147
147
|
getMyFilesShareMemberEmailKey(): Promise<{
|
|
148
|
-
addressId: string;
|
|
149
|
-
addressKey: PrivateKey;
|
|
150
|
-
}>;
|
|
151
|
-
getContextShareMemberEmailKey(shareId: string): Promise<{
|
|
152
148
|
email: string;
|
|
153
149
|
addressId: string;
|
|
154
150
|
addressKey: PrivateKey;
|
|
@@ -3,7 +3,7 @@ import { SharingAPIService } from './apiService';
|
|
|
3
3
|
import { SharingCache } from './cache';
|
|
4
4
|
import { SharingCryptoService } from './cryptoService';
|
|
5
5
|
import { SharesService, NodesService } from './interface';
|
|
6
|
-
import { SharingAccess } from './sharingAccess';
|
|
6
|
+
import { SharingAccess, BATCH_LOADING_SIZE } from './sharingAccess';
|
|
7
7
|
|
|
8
8
|
describe('SharingAccess', () => {
|
|
9
9
|
let apiService: SharingAPIService;
|
|
@@ -14,7 +14,7 @@ describe('SharingAccess', () => {
|
|
|
14
14
|
|
|
15
15
|
let sharingAccess: SharingAccess;
|
|
16
16
|
|
|
17
|
-
const nodeUids = Array.from({ length:
|
|
17
|
+
const nodeUids = Array.from({ length: BATCH_LOADING_SIZE + 5 }, (_, i) => `nodeUid${i}`);
|
|
18
18
|
const nodes = nodeUids.map((nodeUid) => ({ nodeUid }));
|
|
19
19
|
const nodeUidsIterator = async function* () {
|
|
20
20
|
for (const nodeUid of nodeUids) {
|
|
@@ -84,7 +84,7 @@ describe('SharingAccess', () => {
|
|
|
84
84
|
|
|
85
85
|
expect(result).toEqual(nodes);
|
|
86
86
|
expect(apiService.iterateSharedNodeUids).toHaveBeenCalledWith('volumeId', undefined);
|
|
87
|
-
expect(nodesService.iterateNodes).toHaveBeenCalledTimes(2); //
|
|
87
|
+
expect(nodesService.iterateNodes).toHaveBeenCalledTimes(2); // Mocked is a bit more over one batch
|
|
88
88
|
expect(cache.setSharedByMeNodeUids).toHaveBeenCalledWith(nodeUids);
|
|
89
89
|
});
|
|
90
90
|
});
|
|
@@ -107,7 +107,7 @@ describe('SharingAccess', () => {
|
|
|
107
107
|
|
|
108
108
|
expect(result).toEqual(nodes);
|
|
109
109
|
expect(apiService.iterateSharedWithMeNodeUids).toHaveBeenCalledWith(undefined);
|
|
110
|
-
expect(nodesService.iterateNodes).toHaveBeenCalledTimes(2); //
|
|
110
|
+
expect(nodesService.iterateNodes).toHaveBeenCalledTimes(2); // Mocked is a bit more over one batch
|
|
111
111
|
expect(cache.setSharedWithMeNodeUids).toHaveBeenCalledWith(nodeUids);
|
|
112
112
|
});
|
|
113
113
|
});
|
|
@@ -116,6 +116,7 @@ describe('SharingAccess', () => {
|
|
|
116
116
|
it('should return decrypted bookmark', async () => {
|
|
117
117
|
cryptoService.decryptBookmark = jest.fn().mockResolvedValue({
|
|
118
118
|
url: resultOk('url'),
|
|
119
|
+
customPassword: resultOk('customPassword'),
|
|
119
120
|
nodeName: resultOk('nodeName'),
|
|
120
121
|
});
|
|
121
122
|
|
|
@@ -126,6 +127,7 @@ describe('SharingAccess', () => {
|
|
|
126
127
|
uid: 'tokenId',
|
|
127
128
|
creationTime: new Date('2025-01-01'),
|
|
128
129
|
url: 'url',
|
|
130
|
+
customPassword: 'customPassword',
|
|
129
131
|
node: {
|
|
130
132
|
name: 'nodeName',
|
|
131
133
|
type: NodeType.File,
|
|
@@ -138,6 +140,7 @@ describe('SharingAccess', () => {
|
|
|
138
140
|
it('should return degraded bookmark if URL password cannot be decrypted', async () => {
|
|
139
141
|
cryptoService.decryptBookmark = jest.fn().mockResolvedValue({
|
|
140
142
|
url: resultError('url cannot be decrypted'),
|
|
143
|
+
customPassword: resultOk('url cannot be decrypted'),
|
|
141
144
|
nodeName: resultError('url cannot be decrypted'),
|
|
142
145
|
});
|
|
143
146
|
|
|
@@ -148,6 +151,7 @@ describe('SharingAccess', () => {
|
|
|
148
151
|
uid: 'tokenId',
|
|
149
152
|
creationTime: new Date('2025-01-01'),
|
|
150
153
|
url: resultError('url cannot be decrypted'),
|
|
154
|
+
customPassword: resultOk('url cannot be decrypted'),
|
|
151
155
|
node: {
|
|
152
156
|
name: resultError('url cannot be decrypted'),
|
|
153
157
|
type: NodeType.File,
|
|
@@ -160,6 +164,7 @@ describe('SharingAccess', () => {
|
|
|
160
164
|
it('should return degraded bookmark if node name cannot be decrypted', async () => {
|
|
161
165
|
cryptoService.decryptBookmark = jest.fn().mockResolvedValue({
|
|
162
166
|
url: resultOk('url'),
|
|
167
|
+
customPassword: resultOk(undefined),
|
|
163
168
|
nodeName: resultError('node name cannot be decrypted'),
|
|
164
169
|
});
|
|
165
170
|
|
|
@@ -170,6 +175,7 @@ describe('SharingAccess', () => {
|
|
|
170
175
|
uid: 'tokenId',
|
|
171
176
|
creationTime: new Date('2025-01-01'),
|
|
172
177
|
url: resultOk('url'),
|
|
178
|
+
customPassword: resultOk(undefined),
|
|
173
179
|
node: {
|
|
174
180
|
name: resultError('node name cannot be decrypted'),
|
|
175
181
|
type: NodeType.File,
|
|
@@ -9,6 +9,10 @@ import { SharingCache } from './cache';
|
|
|
9
9
|
import { SharingCryptoService } from './cryptoService';
|
|
10
10
|
import { SharesService, NodesService } from './interface';
|
|
11
11
|
|
|
12
|
+
// This is the number of nodes that are loaded in parallel.
|
|
13
|
+
// It is a trade-off between initial wait time and overhead of API calls.
|
|
14
|
+
export const BATCH_LOADING_SIZE = 30;
|
|
15
|
+
|
|
12
16
|
/**
|
|
13
17
|
* Provides high-level actions for access shared nodes.
|
|
14
18
|
*
|
|
@@ -66,6 +70,7 @@ export class SharingAccess {
|
|
|
66
70
|
): AsyncGenerator<DecryptedNode> {
|
|
67
71
|
const batchLoading = new BatchLoading<string, DecryptedNode>({
|
|
68
72
|
iterateItems: (nodeUids) => this.iterateNodesAndIgnoreMissingOnes(nodeUids, signal),
|
|
73
|
+
batchSize: BATCH_LOADING_SIZE,
|
|
69
74
|
});
|
|
70
75
|
for (const nodeUid of nodeUids) {
|
|
71
76
|
yield* batchLoading.load(nodeUid);
|
|
@@ -81,6 +86,7 @@ export class SharingAccess {
|
|
|
81
86
|
const loadedNodeUids = [];
|
|
82
87
|
const batchLoading = new BatchLoading<string, DecryptedNode>({
|
|
83
88
|
iterateItems: (nodeUids) => this.iterateNodesAndIgnoreMissingOnes(nodeUids, signal),
|
|
89
|
+
batchSize: BATCH_LOADING_SIZE,
|
|
84
90
|
});
|
|
85
91
|
for await (const nodeUid of nodeUidsIterator) {
|
|
86
92
|
loadedNodeUids.push(nodeUid);
|
|
@@ -140,13 +146,14 @@ export class SharingAccess {
|
|
|
140
146
|
|
|
141
147
|
async *iterateBookmarks(signal?: AbortSignal): AsyncGenerator<MaybeBookmark> {
|
|
142
148
|
for await (const bookmark of this.apiService.iterateBookmarks(signal)) {
|
|
143
|
-
const { url, nodeName } = await this.cryptoService.decryptBookmark(bookmark);
|
|
149
|
+
const { url, customPassword, nodeName } = await this.cryptoService.decryptBookmark(bookmark);
|
|
144
150
|
|
|
145
|
-
if (!url.ok || !nodeName.ok) {
|
|
151
|
+
if (!url.ok || !customPassword.ok || !nodeName.ok) {
|
|
146
152
|
yield resultError({
|
|
147
153
|
uid: bookmark.tokenId,
|
|
148
154
|
creationTime: bookmark.creationTime,
|
|
149
155
|
url: url,
|
|
156
|
+
customPassword,
|
|
150
157
|
node: {
|
|
151
158
|
name: nodeName,
|
|
152
159
|
type: bookmark.node.type,
|
|
@@ -158,6 +165,7 @@ export class SharingAccess {
|
|
|
158
165
|
uid: bookmark.tokenId,
|
|
159
166
|
creationTime: bookmark.creationTime,
|
|
160
167
|
url: url.value,
|
|
168
|
+
customPassword: customPassword.value,
|
|
161
169
|
node: {
|
|
162
170
|
name: nodeName.value,
|
|
163
171
|
type: bookmark.node.type,
|
|
@@ -58,11 +58,9 @@ describe('SharingManagement', () => {
|
|
|
58
58
|
};
|
|
59
59
|
// @ts-expect-error No need to implement all methods for mocking
|
|
60
60
|
cryptoService = {
|
|
61
|
-
generateShareKeys: jest
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
shareKey: { encrypted: 'encrypted-key', decrypted: { passphraseSessionKey: 'pass-session-key' } },
|
|
65
|
-
}),
|
|
61
|
+
generateShareKeys: jest.fn().mockResolvedValue({
|
|
62
|
+
shareKey: { encrypted: 'encrypted-key', decrypted: { passphraseSessionKey: 'pass-session-key' } },
|
|
63
|
+
}),
|
|
66
64
|
decryptShare: jest.fn().mockImplementation((share) => share),
|
|
67
65
|
decryptInvitation: jest.fn().mockImplementation((invitation) => invitation),
|
|
68
66
|
decryptExternalInvitation: jest.fn().mockImplementation((invitation) => invitation),
|
|
@@ -70,7 +68,7 @@ describe('SharingManagement', () => {
|
|
|
70
68
|
encryptInvitation: jest.fn().mockImplementation(() => {}),
|
|
71
69
|
encryptExternalInvitation: jest.fn().mockImplementation((invitation) => ({
|
|
72
70
|
...invitation,
|
|
73
|
-
base64ExternalInvitationSignature: '
|
|
71
|
+
base64ExternalInvitationSignature: 'external-signature',
|
|
74
72
|
})),
|
|
75
73
|
decryptPublicLink: jest.fn().mockImplementation((publicLink) => publicLink),
|
|
76
74
|
generatePublicLinkPassword: jest.fn().mockResolvedValue('generatedPassword'),
|
|
@@ -85,17 +83,12 @@ describe('SharingManagement', () => {
|
|
|
85
83
|
};
|
|
86
84
|
// @ts-expect-error No need to implement all methods for mocking
|
|
87
85
|
sharesService = {
|
|
88
|
-
loadEncryptedShare: jest
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
passphraseSessionKey: 'sharePassphraseSessionKey',
|
|
95
|
-
}),
|
|
96
|
-
getContextShareMemberEmailKey: jest
|
|
97
|
-
.fn()
|
|
98
|
-
.mockResolvedValue({ email: 'volume-email', addressId: 'addressId', addressKey: 'volume-key' }),
|
|
86
|
+
loadEncryptedShare: jest.fn().mockResolvedValue({
|
|
87
|
+
id: 'shareId',
|
|
88
|
+
addressId: 'addressId',
|
|
89
|
+
creatorEmail: 'address@example.com',
|
|
90
|
+
passphraseSessionKey: 'sharePassphraseSessionKey',
|
|
91
|
+
}),
|
|
99
92
|
};
|
|
100
93
|
// @ts-expect-error No need to implement all methods for mocking
|
|
101
94
|
nodesService = {
|
|
@@ -196,13 +189,11 @@ describe('SharingManagement', () => {
|
|
|
196
189
|
const nodeUid = 'volumeId~nodeUid';
|
|
197
190
|
|
|
198
191
|
it('should create share if no exists', async () => {
|
|
199
|
-
nodesService.getNode = jest
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
name: { ok: true, value: 'name' },
|
|
205
|
-
}));
|
|
192
|
+
nodesService.getNode = jest.fn().mockImplementation((nodeUid) => ({
|
|
193
|
+
nodeUid,
|
|
194
|
+
parentUid: 'parentUid',
|
|
195
|
+
name: { ok: true, value: 'name' },
|
|
196
|
+
}));
|
|
206
197
|
nodesService.notifyNodeChanged = jest.fn();
|
|
207
198
|
|
|
208
199
|
const sharingInfo = await sharingManagement.shareNode(nodeUid, { users: ['email'] });
|
|
@@ -347,6 +338,24 @@ describe('SharingManagement', () => {
|
|
|
347
338
|
expect(apiService.updateInvitation).not.toHaveBeenCalled();
|
|
348
339
|
expect(apiService.inviteProtonUser).not.toHaveBeenCalled();
|
|
349
340
|
});
|
|
341
|
+
|
|
342
|
+
it('should use address from the root node context share', async () => {
|
|
343
|
+
nodesService.getRootNodeEmailKey = jest
|
|
344
|
+
.fn()
|
|
345
|
+
.mockResolvedValue({ email: 'my-volume-email', addressKey: 'my-volume-key' });
|
|
346
|
+
|
|
347
|
+
await sharingManagement.shareNode(nodeUid, { users: ['email'] });
|
|
348
|
+
|
|
349
|
+
expect(apiService.inviteProtonUser).toHaveBeenCalledWith(
|
|
350
|
+
'shareId',
|
|
351
|
+
{
|
|
352
|
+
addedByEmail: 'my-volume-email',
|
|
353
|
+
inviteeEmail: 'email',
|
|
354
|
+
role: 'viewer',
|
|
355
|
+
},
|
|
356
|
+
expect.anything(),
|
|
357
|
+
);
|
|
358
|
+
});
|
|
350
359
|
});
|
|
351
360
|
|
|
352
361
|
describe('external invitations', () => {
|
|
@@ -434,6 +443,27 @@ describe('SharingManagement', () => {
|
|
|
434
443
|
expect(apiService.updateExternalInvitation).not.toHaveBeenCalled();
|
|
435
444
|
expect(apiService.inviteExternalUser).not.toHaveBeenCalled();
|
|
436
445
|
});
|
|
446
|
+
|
|
447
|
+
it('should use address from the root node context share', async () => {
|
|
448
|
+
nodesService.getRootNodeEmailKey = jest.fn().mockResolvedValue({
|
|
449
|
+
email: 'my-volume-email',
|
|
450
|
+
addressId: 'my-volume-addressId',
|
|
451
|
+
addressKey: 'my-volume-key',
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
await sharingManagement.shareNode(nodeUid, { users: ['email'] });
|
|
455
|
+
|
|
456
|
+
expect(apiService.inviteExternalUser).toHaveBeenCalledWith(
|
|
457
|
+
'shareId',
|
|
458
|
+
{
|
|
459
|
+
inviterAddressId: 'my-volume-addressId',
|
|
460
|
+
inviteeEmail: 'email',
|
|
461
|
+
role: 'viewer',
|
|
462
|
+
base64Signature: 'external-signature',
|
|
463
|
+
},
|
|
464
|
+
expect.anything(),
|
|
465
|
+
);
|
|
466
|
+
});
|
|
437
467
|
});
|
|
438
468
|
|
|
439
469
|
describe('mix of internal and external invitations', () => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { c } from 'ttag';
|
|
2
2
|
|
|
3
|
-
import { SessionKey } from '../../crypto';
|
|
3
|
+
import { PrivateKey, SessionKey } from '../../crypto';
|
|
4
4
|
import { ValidationError } from '../../errors';
|
|
5
5
|
import {
|
|
6
6
|
Logger,
|
|
@@ -33,6 +33,12 @@ interface Share {
|
|
|
33
33
|
passphraseSessionKey: SessionKey;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
interface ContextShareAddress {
|
|
37
|
+
addressId: string;
|
|
38
|
+
addressKey: PrivateKey;
|
|
39
|
+
email: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
36
42
|
interface EmailOptions {
|
|
37
43
|
message?: string;
|
|
38
44
|
nodeName?: string;
|
|
@@ -138,18 +144,22 @@ export class SharingManagement {
|
|
|
138
144
|
throw new ValidationError(c('Error').t`Expiration date cannot be in the past`);
|
|
139
145
|
}
|
|
140
146
|
|
|
147
|
+
let contextShareAddress: ContextShareAddress;
|
|
141
148
|
let currentSharing = await this.getInternalSharingInfo(nodeUid);
|
|
142
|
-
if (
|
|
149
|
+
if (currentSharing) {
|
|
150
|
+
contextShareAddress = await this.nodesService.getRootNodeEmailKey(nodeUid);
|
|
151
|
+
} else {
|
|
143
152
|
const node = await this.nodesService.getNode(nodeUid);
|
|
144
|
-
const
|
|
153
|
+
const result = await this.createShare(nodeUid);
|
|
145
154
|
currentSharing = {
|
|
146
|
-
share,
|
|
155
|
+
share: result.share,
|
|
147
156
|
nodeName: node.name.ok ? node.name.value : node.name.error.name,
|
|
148
157
|
protonInvitations: [],
|
|
149
158
|
nonProtonInvitations: [],
|
|
150
159
|
members: [],
|
|
151
160
|
publicLink: undefined,
|
|
152
161
|
};
|
|
162
|
+
contextShareAddress = result.contextShareAddress;
|
|
153
163
|
}
|
|
154
164
|
|
|
155
165
|
const emailOptions: EmailOptions = {
|
|
@@ -187,7 +197,13 @@ export class SharingManagement {
|
|
|
187
197
|
}
|
|
188
198
|
|
|
189
199
|
this.logger.info(`Inviting user ${email} with role ${role} to node ${nodeUid}`);
|
|
190
|
-
const invitation = await this.inviteProtonUser(
|
|
200
|
+
const invitation = await this.inviteProtonUser(
|
|
201
|
+
contextShareAddress,
|
|
202
|
+
currentSharing.share,
|
|
203
|
+
email,
|
|
204
|
+
role,
|
|
205
|
+
emailOptions,
|
|
206
|
+
);
|
|
191
207
|
currentSharing.protonInvitations.push(invitation);
|
|
192
208
|
}
|
|
193
209
|
|
|
@@ -225,7 +241,13 @@ export class SharingManagement {
|
|
|
225
241
|
}
|
|
226
242
|
|
|
227
243
|
this.logger.info(`Inviting external user ${email} with role ${role} to node ${nodeUid}`);
|
|
228
|
-
const invitation = await this.inviteExternalUser(
|
|
244
|
+
const invitation = await this.inviteExternalUser(
|
|
245
|
+
contextShareAddress,
|
|
246
|
+
currentSharing.share,
|
|
247
|
+
email,
|
|
248
|
+
role,
|
|
249
|
+
emailOptions,
|
|
250
|
+
);
|
|
229
251
|
currentSharing.nonProtonInvitations.push(invitation);
|
|
230
252
|
}
|
|
231
253
|
|
|
@@ -241,7 +263,7 @@ export class SharingManagement {
|
|
|
241
263
|
);
|
|
242
264
|
} else {
|
|
243
265
|
this.logger.info(`Sharing via public link with role ${options.role} to node ${nodeUid}`);
|
|
244
|
-
currentSharing.publicLink = await this.shareViaLink(currentSharing.share, options);
|
|
266
|
+
currentSharing.publicLink = await this.shareViaLink(contextShareAddress, currentSharing.share, options);
|
|
245
267
|
}
|
|
246
268
|
}
|
|
247
269
|
|
|
@@ -371,7 +393,7 @@ export class SharingManagement {
|
|
|
371
393
|
};
|
|
372
394
|
}
|
|
373
395
|
|
|
374
|
-
private async createShare(nodeUid: string): Promise<Share> {
|
|
396
|
+
private async createShare(nodeUid: string): Promise<{ share: Share; contextShareAddress: ContextShareAddress }> {
|
|
375
397
|
const node = await this.nodesService.getNode(nodeUid);
|
|
376
398
|
if (!node.parentUid) {
|
|
377
399
|
throw new ValidationError(c('Error').t`Cannot share root folder`);
|
|
@@ -387,12 +409,22 @@ export class SharingManagement {
|
|
|
387
409
|
base64NameKeyPacket: keys.base64NameKeyPacket,
|
|
388
410
|
});
|
|
389
411
|
await this.nodesService.notifyNodeChanged(nodeUid);
|
|
390
|
-
|
|
412
|
+
|
|
413
|
+
const share = {
|
|
391
414
|
volumeId,
|
|
392
415
|
shareId,
|
|
393
416
|
creatorEmail: email,
|
|
394
417
|
passphraseSessionKey: keys.shareKey.decrypted.passphraseSessionKey,
|
|
395
418
|
};
|
|
419
|
+
const contextShareAddress = {
|
|
420
|
+
email,
|
|
421
|
+
addressId,
|
|
422
|
+
addressKey,
|
|
423
|
+
};
|
|
424
|
+
return {
|
|
425
|
+
share,
|
|
426
|
+
contextShareAddress,
|
|
427
|
+
};
|
|
396
428
|
}
|
|
397
429
|
|
|
398
430
|
private async deleteShare(shareId: string, nodeUid: string): Promise<void> {
|
|
@@ -401,12 +433,12 @@ export class SharingManagement {
|
|
|
401
433
|
}
|
|
402
434
|
|
|
403
435
|
private async inviteProtonUser(
|
|
436
|
+
inviter: ContextShareAddress,
|
|
404
437
|
share: Share,
|
|
405
438
|
inviteeEmail: string,
|
|
406
439
|
role: MemberRole,
|
|
407
440
|
emailOptions: EmailOptions,
|
|
408
441
|
): Promise<ProtonInvitation> {
|
|
409
|
-
const inviter = await this.sharesService.getContextShareMemberEmailKey(share.shareId);
|
|
410
442
|
const invitationCrypto = await this.cryptoService.encryptInvitation(
|
|
411
443
|
share.passphraseSessionKey,
|
|
412
444
|
inviter.addressKey,
|
|
@@ -461,12 +493,12 @@ export class SharingManagement {
|
|
|
461
493
|
}
|
|
462
494
|
|
|
463
495
|
private async inviteExternalUser(
|
|
496
|
+
inviter: ContextShareAddress,
|
|
464
497
|
share: Share,
|
|
465
498
|
inviteeEmail: string,
|
|
466
499
|
role: MemberRole,
|
|
467
500
|
emailOptions: EmailOptions,
|
|
468
501
|
): Promise<NonProtonInvitation> {
|
|
469
|
-
const inviter = await this.sharesService.getContextShareMemberEmailKey(share.shareId);
|
|
470
502
|
const invitationCrypto = await this.cryptoService.encryptExternalInvitation(
|
|
471
503
|
share.passphraseSessionKey,
|
|
472
504
|
inviter.addressKey,
|
|
@@ -515,21 +547,20 @@ export class SharingManagement {
|
|
|
515
547
|
}
|
|
516
548
|
|
|
517
549
|
private async shareViaLink(
|
|
550
|
+
inviter: ContextShareAddress,
|
|
518
551
|
share: Share,
|
|
519
552
|
options: SharePublicLinkSettingsObject,
|
|
520
553
|
): Promise<PublicLinkWithCreatorEmail> {
|
|
521
|
-
const { email: creatorEmail } = await this.sharesService.getContextShareMemberEmailKey(share.shareId);
|
|
522
|
-
|
|
523
554
|
const generatedPassword = await this.cryptoService.generatePublicLinkPassword();
|
|
524
555
|
const password = options.customPassword ? `${generatedPassword}${options.customPassword}` : generatedPassword;
|
|
525
556
|
|
|
526
557
|
const { crypto, srp } = await this.cryptoService.encryptPublicLink(
|
|
527
|
-
|
|
558
|
+
inviter.email,
|
|
528
559
|
share.passphraseSessionKey,
|
|
529
560
|
password,
|
|
530
561
|
);
|
|
531
562
|
const publicLink = await this.apiService.createPublicLink(share.shareId, {
|
|
532
|
-
creatorEmail,
|
|
563
|
+
creatorEmail: inviter.email,
|
|
533
564
|
role: options.role,
|
|
534
565
|
includesCustomPassword: !!options.customPassword,
|
|
535
566
|
expirationTime: options.expiration ? Math.floor(options.expiration.getTime() / 1000) : undefined,
|
|
@@ -545,7 +576,7 @@ export class SharingManagement {
|
|
|
545
576
|
customPassword: options.customPassword,
|
|
546
577
|
expirationTime: options.expiration,
|
|
547
578
|
numberOfInitializedDownloads: 0,
|
|
548
|
-
creatorEmail,
|
|
579
|
+
creatorEmail: inviter.email,
|
|
549
580
|
};
|
|
550
581
|
}
|
|
551
582
|
|
|
@@ -206,7 +206,7 @@ describe('UploadManager', () => {
|
|
|
206
206
|
await promise;
|
|
207
207
|
} catch (error: any) {
|
|
208
208
|
expect(error.message).toBe('Draft already exists');
|
|
209
|
-
expect(error.
|
|
209
|
+
expect(error.isUnfinishedUpload).toBe(true);
|
|
210
210
|
}
|
|
211
211
|
expect(apiService.deleteDraft).not.toHaveBeenCalled();
|
|
212
212
|
});
|
|
@@ -237,7 +237,7 @@ describe('UploadManager', () => {
|
|
|
237
237
|
await promise;
|
|
238
238
|
} catch (error: any) {
|
|
239
239
|
expect(error.message).toBe('Draft already exists');
|
|
240
|
-
expect(error.
|
|
240
|
+
expect(error.isUnfinishedUpload).toBe(true);
|
|
241
241
|
}
|
|
242
242
|
expect(apiService.deleteDraft).not.toHaveBeenCalled();
|
|
243
243
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { c } from 'ttag';
|
|
2
2
|
|
|
3
3
|
import { Logger, ProtonDriveTelemetry, UploadMetadata } from '../../interface';
|
|
4
|
-
import { ValidationError,
|
|
4
|
+
import { ValidationError, NodeWithSameNameExistsValidationError } from '../../errors';
|
|
5
5
|
import { ErrorCode } from '../apiService';
|
|
6
6
|
import { generateFileExtendedAttributes } from '../nodes';
|
|
7
7
|
import { UploadAPIService } from './apiService';
|
|
@@ -158,9 +158,7 @@ export class UploadManager {
|
|
|
158
158
|
? makeNodeUid(splitNodeUid(parentFolderUid).volumeId, typedDetails.ConflictLinkID)
|
|
159
159
|
: undefined;
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
// that includes the available name the client can use.
|
|
163
|
-
throw new NodeAlreadyExistsValidationError(
|
|
161
|
+
throw new NodeWithSameNameExistsValidationError(
|
|
164
162
|
error.message,
|
|
165
163
|
error.code,
|
|
166
164
|
existingNodeUid,
|
package/src/protonDriveClient.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
MaybeMissingNode,
|
|
7
7
|
NodeResult,
|
|
8
8
|
Revision,
|
|
9
|
+
RevisionOrUid,
|
|
9
10
|
ShareNodeSettings,
|
|
10
11
|
UnshareNodeSettings,
|
|
11
12
|
ProtonInvitationOrUid,
|
|
@@ -311,7 +312,7 @@ export class ProtonDriveClient {
|
|
|
311
312
|
* @param nodeUid - Node entity or its UID string.
|
|
312
313
|
* @returns The updated node entity.
|
|
313
314
|
* @throws {@link ValidationError} If the name is empty, too long, or contains a slash.
|
|
314
|
-
* @throws {@link
|
|
315
|
+
* @throws {@link ValidationError} If another node with the same name already exists.
|
|
315
316
|
*/
|
|
316
317
|
async renameNode(nodeUid: NodeOrUid, newName: string): Promise<MaybeNode> {
|
|
317
318
|
this.logger.info(`Renaming node ${getUid(nodeUid)}`);
|
|
@@ -342,7 +343,7 @@ export class ProtonDriveClient {
|
|
|
342
343
|
newParentNodeUid: NodeOrUid,
|
|
343
344
|
signal?: AbortSignal,
|
|
344
345
|
): AsyncGenerator<NodeResult> {
|
|
345
|
-
this.logger.info(`Moving ${nodeUids.length} nodes to ${newParentNodeUid}`);
|
|
346
|
+
this.logger.info(`Moving ${nodeUids.length} nodes to ${getUid(newParentNodeUid)}`);
|
|
346
347
|
yield* this.nodes.management.moveNodes(getUids(nodeUids), getUid(newParentNodeUid), signal);
|
|
347
348
|
}
|
|
348
349
|
|
|
@@ -453,9 +454,9 @@ export class ProtonDriveClient {
|
|
|
453
454
|
*
|
|
454
455
|
* @param revisionUid - UID of the revision to restore.
|
|
455
456
|
*/
|
|
456
|
-
async restoreRevision(revisionUid:
|
|
457
|
-
this.logger.info(`Restoring revision ${revisionUid}`);
|
|
458
|
-
await this.nodes.revisions.restoreRevision(revisionUid);
|
|
457
|
+
async restoreRevision(revisionUid: RevisionOrUid): Promise<void> {
|
|
458
|
+
this.logger.info(`Restoring revision ${getUid(revisionUid)}`);
|
|
459
|
+
await this.nodes.revisions.restoreRevision(getUid(revisionUid));
|
|
459
460
|
}
|
|
460
461
|
|
|
461
462
|
/**
|
|
@@ -463,9 +464,9 @@ export class ProtonDriveClient {
|
|
|
463
464
|
*
|
|
464
465
|
* @param revisionUid - UID of the revision to delete.
|
|
465
466
|
*/
|
|
466
|
-
async deleteRevision(revisionUid:
|
|
467
|
-
this.logger.info(`Deleting revision ${revisionUid}`);
|
|
468
|
-
await this.nodes.revisions.deleteRevision(revisionUid);
|
|
467
|
+
async deleteRevision(revisionUid: RevisionOrUid): Promise<void> {
|
|
468
|
+
this.logger.info(`Deleting revision ${getUid(revisionUid)}`);
|
|
469
|
+
await this.nodes.revisions.deleteRevision(getUid(revisionUid));
|
|
469
470
|
}
|
|
470
471
|
|
|
471
472
|
/**
|
|
@@ -527,21 +528,21 @@ export class ProtonDriveClient {
|
|
|
527
528
|
/**
|
|
528
529
|
* Accept the invitation to the shared node.
|
|
529
530
|
*
|
|
530
|
-
* @param
|
|
531
|
+
* @param invitationUid - Invitation entity or its UID string.
|
|
531
532
|
*/
|
|
532
|
-
async acceptInvitation(
|
|
533
|
-
this.logger.info(`Accepting invitation ${
|
|
534
|
-
await this.sharing.access.acceptInvitation(
|
|
533
|
+
async acceptInvitation(invitationUid: ProtonInvitationOrUid): Promise<void> {
|
|
534
|
+
this.logger.info(`Accepting invitation ${getUid(invitationUid)}`);
|
|
535
|
+
await this.sharing.access.acceptInvitation(getUid(invitationUid));
|
|
535
536
|
}
|
|
536
537
|
|
|
537
538
|
/**
|
|
538
539
|
* Reject the invitation to the shared node.
|
|
539
540
|
*
|
|
540
|
-
* @param
|
|
541
|
+
* @param invitationOrUid - Invitation entity or its UID string.
|
|
541
542
|
*/
|
|
542
|
-
async rejectInvitation(
|
|
543
|
-
this.logger.info(`Rejecting invitation ${
|
|
544
|
-
await this.sharing.access.rejectInvitation(
|
|
543
|
+
async rejectInvitation(invitationUid: ProtonInvitationOrUid): Promise<void> {
|
|
544
|
+
this.logger.info(`Rejecting invitation ${getUid(invitationUid)}`);
|
|
545
|
+
await this.sharing.access.rejectInvitation(getUid(invitationUid));
|
|
545
546
|
}
|
|
546
547
|
|
|
547
548
|
/**
|