@protontech/drive-sdk 0.6.1 → 0.7.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 (119) hide show
  1. package/dist/featureFlags.d.ts +7 -0
  2. package/dist/featureFlags.js +14 -0
  3. package/dist/featureFlags.js.map +1 -0
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.js +3 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/interface/featureFlags.d.ts +7 -0
  8. package/dist/interface/featureFlags.js +3 -0
  9. package/dist/interface/featureFlags.js.map +1 -0
  10. package/dist/interface/index.d.ts +3 -0
  11. package/dist/interface/index.js.map +1 -1
  12. package/dist/internal/apiService/apiService.d.ts +2 -2
  13. package/dist/internal/apiService/apiService.js.map +1 -1
  14. package/dist/internal/apiService/errors.js +4 -3
  15. package/dist/internal/apiService/errors.js.map +1 -1
  16. package/dist/internal/download/cryptoService.js +8 -6
  17. package/dist/internal/download/cryptoService.js.map +1 -1
  18. package/dist/internal/download/fileDownloader.d.ts +2 -1
  19. package/dist/internal/download/fileDownloader.js +6 -3
  20. package/dist/internal/download/fileDownloader.js.map +1 -1
  21. package/dist/internal/download/index.d.ts +1 -1
  22. package/dist/internal/download/index.js +3 -3
  23. package/dist/internal/download/index.js.map +1 -1
  24. package/dist/internal/nodes/apiService.d.ts +9 -8
  25. package/dist/internal/nodes/apiService.js +14 -5
  26. package/dist/internal/nodes/apiService.js.map +1 -1
  27. package/dist/internal/nodes/apiService.test.js +5 -5
  28. package/dist/internal/nodes/apiService.test.js.map +1 -1
  29. package/dist/internal/nodes/cryptoReporter.d.ts +3 -3
  30. package/dist/internal/nodes/cryptoReporter.js.map +1 -1
  31. package/dist/internal/nodes/cryptoService.d.ts +12 -21
  32. package/dist/internal/nodes/cryptoService.js +45 -14
  33. package/dist/internal/nodes/cryptoService.js.map +1 -1
  34. package/dist/internal/nodes/cryptoService.test.js +262 -17
  35. package/dist/internal/nodes/cryptoService.test.js.map +1 -1
  36. package/dist/internal/nodes/interface.d.ts +13 -3
  37. package/dist/internal/nodes/nodesAccess.d.ts +8 -1
  38. package/dist/internal/nodes/nodesAccess.js +13 -0
  39. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  40. package/dist/internal/nodes/nodesManagement.d.ts +4 -4
  41. package/dist/internal/nodes/nodesManagement.js +16 -20
  42. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  43. package/dist/internal/nodes/nodesManagement.test.js +21 -10
  44. package/dist/internal/nodes/nodesManagement.test.js.map +1 -1
  45. package/dist/internal/photos/upload.d.ts +2 -2
  46. package/dist/internal/photos/upload.js +1 -1
  47. package/dist/internal/photos/upload.js.map +1 -1
  48. package/dist/internal/sharingPublic/index.d.ts +6 -6
  49. package/dist/internal/sharingPublic/index.js +8 -7
  50. package/dist/internal/sharingPublic/index.js.map +1 -1
  51. package/dist/internal/sharingPublic/nodes.d.ts +16 -3
  52. package/dist/internal/sharingPublic/nodes.js +34 -2
  53. package/dist/internal/sharingPublic/nodes.js.map +1 -1
  54. package/dist/internal/sharingPublic/unauthApiService.d.ts +17 -0
  55. package/dist/internal/sharingPublic/unauthApiService.js +31 -0
  56. package/dist/internal/sharingPublic/unauthApiService.js.map +1 -0
  57. package/dist/internal/sharingPublic/unauthApiService.test.d.ts +1 -0
  58. package/dist/internal/sharingPublic/unauthApiService.test.js +27 -0
  59. package/dist/internal/sharingPublic/unauthApiService.test.js.map +1 -0
  60. package/dist/internal/upload/apiService.d.ts +4 -3
  61. package/dist/internal/upload/apiService.js.map +1 -1
  62. package/dist/internal/upload/cryptoService.d.ts +8 -3
  63. package/dist/internal/upload/cryptoService.js +45 -9
  64. package/dist/internal/upload/cryptoService.js.map +1 -1
  65. package/dist/internal/upload/fileUploader.d.ts +5 -2
  66. package/dist/internal/upload/fileUploader.js +7 -4
  67. package/dist/internal/upload/fileUploader.js.map +1 -1
  68. package/dist/internal/upload/fileUploader.test.js +1 -1
  69. package/dist/internal/upload/fileUploader.test.js.map +1 -1
  70. package/dist/internal/upload/interface.d.ts +25 -13
  71. package/dist/internal/upload/manager.js +7 -4
  72. package/dist/internal/upload/manager.js.map +1 -1
  73. package/dist/internal/upload/manager.test.js +5 -4
  74. package/dist/internal/upload/manager.test.js.map +1 -1
  75. package/dist/internal/upload/streamUploader.js +9 -4
  76. package/dist/internal/upload/streamUploader.js.map +1 -1
  77. package/dist/internal/upload/streamUploader.test.js +8 -5
  78. package/dist/internal/upload/streamUploader.test.js.map +1 -1
  79. package/dist/protonDriveClient.d.ts +2 -2
  80. package/dist/protonDriveClient.js +7 -2
  81. package/dist/protonDriveClient.js.map +1 -1
  82. package/dist/protonDrivePublicLinkClient.d.ts +2 -1
  83. package/dist/protonDrivePublicLinkClient.js +7 -5
  84. package/dist/protonDrivePublicLinkClient.js.map +1 -1
  85. package/package.json +1 -1
  86. package/src/featureFlags.ts +11 -0
  87. package/src/index.ts +1 -0
  88. package/src/interface/featureFlags.ts +7 -0
  89. package/src/interface/index.ts +3 -0
  90. package/src/internal/apiService/apiService.ts +2 -2
  91. package/src/internal/apiService/errors.ts +5 -4
  92. package/src/internal/download/cryptoService.ts +13 -6
  93. package/src/internal/download/fileDownloader.ts +4 -2
  94. package/src/internal/download/index.ts +3 -0
  95. package/src/internal/nodes/apiService.test.ts +5 -5
  96. package/src/internal/nodes/apiService.ts +23 -10
  97. package/src/internal/nodes/cryptoReporter.ts +3 -3
  98. package/src/internal/nodes/cryptoService.test.ts +370 -18
  99. package/src/internal/nodes/cryptoService.ts +73 -32
  100. package/src/internal/nodes/interface.ts +16 -2
  101. package/src/internal/nodes/nodesAccess.ts +17 -0
  102. package/src/internal/nodes/nodesManagement.test.ts +21 -10
  103. package/src/internal/nodes/nodesManagement.ts +20 -24
  104. package/src/internal/photos/upload.ts +3 -3
  105. package/src/internal/sharingPublic/index.ts +7 -3
  106. package/src/internal/sharingPublic/nodes.ts +43 -2
  107. package/src/internal/sharingPublic/unauthApiService.test.ts +29 -0
  108. package/src/internal/sharingPublic/unauthApiService.ts +32 -0
  109. package/src/internal/upload/apiService.ts +4 -3
  110. package/src/internal/upload/cryptoService.ts +73 -12
  111. package/src/internal/upload/fileUploader.test.ts +1 -1
  112. package/src/internal/upload/fileUploader.ts +18 -11
  113. package/src/internal/upload/interface.ts +24 -13
  114. package/src/internal/upload/manager.test.ts +5 -4
  115. package/src/internal/upload/manager.ts +7 -4
  116. package/src/internal/upload/streamUploader.test.ts +8 -5
  117. package/src/internal/upload/streamUploader.ts +10 -4
  118. package/src/protonDriveClient.ts +12 -2
  119. package/src/protonDrivePublicLinkClient.ts +8 -3
@@ -10,6 +10,7 @@ import {
10
10
  MetricVolumeType,
11
11
  Revision,
12
12
  RevisionState,
13
+ AnonymousUser,
13
14
  } from '../../interface';
14
15
 
15
16
  export type FilterOptions = {
@@ -57,8 +58,8 @@ export interface EncryptedNode extends BaseNode {
57
58
  }
58
59
 
59
60
  export interface EncryptedNodeCrypto {
60
- signatureEmail?: string;
61
- nameSignatureEmail?: string;
61
+ signatureEmail?: string | AnonymousUser;
62
+ nameSignatureEmail?: string | AnonymousUser;
62
63
  armoredKey: string;
63
64
  armoredNodePassphrase: string;
64
65
  armoredNodePassphraseSignature?: string;
@@ -88,6 +89,19 @@ export interface EncryptedNodeFolderCrypto extends EncryptedNodeCrypto {
88
89
  // eslint-disable-next-line @typescript-eslint/no-empty-object-type
89
90
  export interface EncryptedNodeAlbumCrypto extends EncryptedNodeCrypto {}
90
91
 
92
+ export type NodeSigningKeys =
93
+ | {
94
+ type: 'userAddress';
95
+ email: string;
96
+ addressId: string;
97
+ key: PrivateKey;
98
+ }
99
+ | {
100
+ type: 'nodeKey';
101
+ nodeKey?: PrivateKey;
102
+ parentNodeKey?: PrivateKey;
103
+ };
104
+
91
105
  /**
92
106
  * Interface used only internally in the nodes module.
93
107
  *
@@ -29,6 +29,7 @@ import {
29
29
  DecryptedNode,
30
30
  DecryptedNodeKeys,
31
31
  FilterOptions,
32
+ NodeSigningKeys,
32
33
  } from './interface';
33
34
  import { validateNodeName } from './validations';
34
35
  import { isProtonDocument, isProtonSheet } from './mediaTypes';
@@ -425,6 +426,22 @@ export class NodesAccess {
425
426
  };
426
427
  }
427
428
 
429
+ async getNodeSigningKeys(
430
+ uids: { nodeUid: string; parentNodeUid?: string } | { nodeUid?: string; parentNodeUid: string },
431
+ ): Promise<NodeSigningKeys> {
432
+ const contextNodeUid = uids.nodeUid || uids.parentNodeUid;
433
+ if (!contextNodeUid) {
434
+ throw new Error('Context node UID is required for signing keys');
435
+ }
436
+ const address = await this.getRootNodeEmailKey(contextNodeUid);
437
+ return {
438
+ type: 'userAddress',
439
+ email: address.email,
440
+ addressId: address.addressId,
441
+ key: address.addressKey,
442
+ };
443
+ }
444
+
428
445
  async getRootNodeEmailKey(nodeUid: string): Promise<{
429
446
  email: string;
430
447
  addressId: string;
@@ -57,7 +57,7 @@ describe('NodesManagement', () => {
57
57
  restoreNodes: jest.fn(async function* (uids) {
58
58
  yield* uids.map((uid) => ({ ok: true, uid }) as NodeResult);
59
59
  }),
60
- deleteNodes: jest.fn(async function* (uids) {
60
+ deleteTrashedNodes: jest.fn(async function* (uids) {
61
61
  yield* uids.map((uid) => ({ ok: true, uid }) as NodeResult);
62
62
  }),
63
63
  createFolder: jest.fn(),
@@ -117,7 +117,12 @@ describe('NodesManagement', () => {
117
117
  nameSessionKey: `${uid}-nameSessionKey`,
118
118
  }),
119
119
  ),
120
- getRootNodeEmailKey: jest.fn().mockResolvedValue({ email: 'root-email', addressKey: 'root-key' }),
120
+ getNodeSigningKeys: jest.fn().mockResolvedValue({
121
+ type: 'userAddress',
122
+ email: 'root-email',
123
+ addressId: 'root-addressId',
124
+ key: 'root-key',
125
+ }),
121
126
  notifyNodeChanged: jest.fn(),
122
127
  notifyNodeDeleted: jest.fn(),
123
128
  notifyChildCreated: jest.fn(),
@@ -136,11 +141,11 @@ describe('NodesManagement', () => {
136
141
  nameAuthor: { ok: true, value: 'newSignatureEmail' },
137
142
  hash: 'newHash',
138
143
  });
139
- expect(nodesAccess.getRootNodeEmailKey).toHaveBeenCalledWith('nodeUid');
144
+ expect(nodesAccess.getNodeSigningKeys).toHaveBeenCalledWith({ nodeUid: 'nodeUid', parentNodeUid: 'parentUid' });
140
145
  expect(cryptoService.encryptNewName).toHaveBeenCalledWith(
141
146
  { key: 'parentUid-key', hashKey: 'parentUid-hashKey' },
142
147
  'nodeUid-nameSessionKey',
143
- { email: 'root-email', addressKey: 'root-key' },
148
+ { type: 'userAddress', email: 'root-email', addressId: 'root-addressId', key: 'root-key' },
144
149
  'new name',
145
150
  );
146
151
  expect(apiService.renameNode).toHaveBeenCalledWith(
@@ -181,7 +186,10 @@ describe('NodesManagement', () => {
181
186
  keyAuthor: { ok: true, value: 'movedSignatureEmail' },
182
187
  nameAuthor: { ok: true, value: 'movedNameSignatureEmail' },
183
188
  });
184
- expect(nodesAccess.getRootNodeEmailKey).toHaveBeenCalledWith('newParentNodeUid');
189
+ expect(nodesAccess.getNodeSigningKeys).toHaveBeenCalledWith({
190
+ nodeUid: 'nodeUid',
191
+ parentNodeUid: 'newParentNodeUid',
192
+ });
185
193
  expect(cryptoService.encryptNodeWithNewParent).toHaveBeenCalledWith(
186
194
  nodes.nodeUid,
187
195
  expect.objectContaining({
@@ -192,7 +200,7 @@ describe('NodesManagement', () => {
192
200
  nameSessionKey: 'nodeUid-nameSessionKey',
193
201
  }),
194
202
  expect.objectContaining({ key: 'newParentNodeUid-key', hashKey: 'newParentNodeUid-hashKey' }),
195
- { email: 'root-email', addressKey: 'root-key' },
203
+ { type: 'userAddress', email: 'root-email', addressId: 'root-addressId', key: 'root-key' },
196
204
  );
197
205
  expect(apiService.moveNode).toHaveBeenCalledWith(
198
206
  'nodeUid',
@@ -232,7 +240,7 @@ describe('NodesManagement', () => {
232
240
  nameSessionKey: 'anonymousNodeUid-nameSessionKey',
233
241
  }),
234
242
  expect.objectContaining({ key: 'newParentNodeUid-key', hashKey: 'newParentNodeUid-hashKey' }),
235
- { email: 'root-email', addressKey: 'root-key' },
243
+ { type: 'userAddress', email: 'root-email', addressId: 'root-addressId', key: 'root-key' },
236
244
  );
237
245
  expect(newNode).toEqual({
238
246
  ...nodes.anonymousNodeUid,
@@ -276,7 +284,10 @@ describe('NodesManagement', () => {
276
284
  keyAuthor: { ok: true, value: 'copiedSignatureEmail' },
277
285
  nameAuthor: { ok: true, value: 'copiedNameSignatureEmail' },
278
286
  });
279
- expect(nodesAccess.getRootNodeEmailKey).toHaveBeenCalledWith('newParentNodeUid');
287
+ expect(nodesAccess.getNodeSigningKeys).toHaveBeenCalledWith({
288
+ nodeUid: 'nodeUid',
289
+ parentNodeUid: 'newParentNodeUid',
290
+ });
280
291
  expect(cryptoService.encryptNodeWithNewParent).toHaveBeenCalledWith(
281
292
  nodes.nodeUid,
282
293
  expect.objectContaining({
@@ -287,7 +298,7 @@ describe('NodesManagement', () => {
287
298
  nameSessionKey: 'nodeUid-nameSessionKey',
288
299
  }),
289
300
  expect.objectContaining({ key: 'newParentNodeUid-key', hashKey: 'newParentNodeUid-hashKey' }),
290
- { email: 'root-email', addressKey: 'root-key' },
301
+ { type: 'userAddress', email: 'root-email', addressId: 'root-addressId', key: 'root-key' },
291
302
  );
292
303
  expect(apiService.copyNode).toHaveBeenCalledWith('nodeUid', {
293
304
  parentUid: 'newParentNodeUid',
@@ -322,7 +333,7 @@ describe('NodesManagement', () => {
322
333
  nameSessionKey: 'anonymousNodeUid-nameSessionKey',
323
334
  }),
324
335
  expect.objectContaining({ key: 'newParentNodeUid-key', hashKey: 'newParentNodeUid-hashKey' }),
325
- { email: 'root-email', addressKey: 'root-key' },
336
+ { type: 'userAddress', email: 'root-email', addressId: 'root-addressId', key: 'root-key' },
326
337
  );
327
338
  expect(newNode).toEqual({
328
339
  ...nodes.anonymousNodeUid,
@@ -28,10 +28,10 @@ const AVAILABLE_NAME_LIMIT = 1000;
28
28
  */
29
29
  export class NodesManagement {
30
30
  constructor(
31
- private apiService: NodeAPIService,
32
- private cryptoCache: NodesCryptoCache,
33
- private cryptoService: NodesCryptoService,
34
- private nodesAccess: NodesAccess,
31
+ protected apiService: NodeAPIService,
32
+ protected cryptoCache: NodesCryptoCache,
33
+ protected cryptoService: NodesCryptoService,
34
+ protected nodesAccess: NodesAccess,
35
35
  ) {
36
36
  this.apiService = apiService;
37
37
  this.cryptoCache = cryptoCache;
@@ -49,7 +49,7 @@ export class NodesManagement {
49
49
  const node = await this.nodesAccess.getNode(nodeUid);
50
50
  const { nameSessionKey: nodeNameSessionKey } = await this.nodesAccess.getNodePrivateAndSessionKeys(nodeUid);
51
51
  const parentKeys = await this.nodesAccess.getParentKeys(node);
52
- const address = await this.nodesAccess.getRootNodeEmailKey(nodeUid);
52
+ const signingKeys = await this.nodesAccess.getNodeSigningKeys({ nodeUid, parentNodeUid: node.parentUid });
53
53
 
54
54
  if (!options.allowRenameRootNode && (!node.hash || !parentKeys.hashKey)) {
55
55
  throw new ValidationError(c('Error').t`Renaming root item is not allowed`);
@@ -58,7 +58,7 @@ export class NodesManagement {
58
58
  const { signatureEmail, armoredNodeName, hash } = await this.cryptoService.encryptNewName(
59
59
  parentKeys,
60
60
  nodeNameSessionKey,
61
- address,
61
+ signingKeys,
62
62
  newName,
63
63
  );
64
64
 
@@ -95,7 +95,7 @@ export class NodesManagement {
95
95
  ...node,
96
96
  name: resultOk(newName),
97
97
  encryptedName: armoredNodeName,
98
- nameAuthor: resultOk(signatureEmail),
98
+ nameAuthor: resultOk(signatureEmail || null),
99
99
  hash,
100
100
  };
101
101
  return newNode;
@@ -124,14 +124,12 @@ export class NodesManagement {
124
124
  }
125
125
 
126
126
  async moveNode(nodeUid: string, newParentUid: string): Promise<DecryptedNode> {
127
- const [node, address] = await Promise.all([
128
- this.nodesAccess.getNode(nodeUid),
129
- this.nodesAccess.getRootNodeEmailKey(newParentUid),
130
- ]);
127
+ const node = await this.nodesAccess.getNode(nodeUid);
131
128
 
132
- const [keys, newParentKeys] = await Promise.all([
129
+ const [keys, newParentKeys, signingKeys] = await Promise.all([
133
130
  this.nodesAccess.getNodePrivateAndSessionKeys(nodeUid),
134
131
  this.nodesAccess.getNodeKeys(newParentUid),
132
+ this.nodesAccess.getNodeSigningKeys({ nodeUid, parentNodeUid: newParentUid }),
135
133
  ]);
136
134
 
137
135
  if (!node.hash) {
@@ -145,7 +143,7 @@ export class NodesManagement {
145
143
  node,
146
144
  keys,
147
145
  { key: newParentKeys.key, hashKey: newParentKeys.hashKey },
148
- address,
146
+ signingKeys,
149
147
  );
150
148
 
151
149
  // Node could be uploaded or renamed by anonymous user and thus have
@@ -214,14 +212,12 @@ export class NodesManagement {
214
212
  }
215
213
 
216
214
  async copyNode(nodeUid: string, newParentUid: string): Promise<DecryptedNode> {
217
- const [node, address] = await Promise.all([
218
- this.nodesAccess.getNode(nodeUid),
219
- this.nodesAccess.getRootNodeEmailKey(newParentUid),
220
- ]);
215
+ const node = await this.nodesAccess.getNode(nodeUid);
221
216
 
222
- const [keys, newParentKeys] = await Promise.all([
217
+ const [keys, newParentKeys, signingKeys] = await Promise.all([
223
218
  this.nodesAccess.getNodePrivateAndSessionKeys(nodeUid),
224
219
  this.nodesAccess.getNodeKeys(newParentUid),
220
+ this.nodesAccess.getNodeSigningKeys({ nodeUid, parentNodeUid: newParentUid }),
225
221
  ]);
226
222
 
227
223
  if (!newParentKeys.hashKey) {
@@ -232,7 +228,7 @@ export class NodesManagement {
232
228
  node,
233
229
  keys,
234
230
  { key: newParentKeys.key, hashKey: newParentKeys.hashKey },
235
- address,
231
+ signingKeys,
236
232
  );
237
233
 
238
234
  // Node could be uploaded or renamed by anonymous user and thus have
@@ -286,7 +282,7 @@ export class NodesManagement {
286
282
  }
287
283
 
288
284
  async *deleteNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
289
- for await (const result of this.apiService.deleteNodes(nodeUids, signal)) {
285
+ for await (const result of this.apiService.deleteTrashedNodes(nodeUids, signal)) {
290
286
  if (result.ok) {
291
287
  await this.nodesAccess.notifyNodeDeleted(result.uid);
292
288
  }
@@ -303,12 +299,12 @@ export class NodesManagement {
303
299
  throw new ValidationError(c('Error').t`Creating folders in non-folders is not allowed`);
304
300
  }
305
301
 
306
- const address = await this.nodesAccess.getRootNodeEmailKey(parentNodeUid);
302
+ const signingKeys = await this.nodesAccess.getNodeSigningKeys({ parentNodeUid });
307
303
  const extendedAttributes = generateFolderExtendedAttributes(modificationTime);
308
304
 
309
305
  const { encryptedCrypto, keys } = await this.cryptoService.createFolder(
310
306
  { key: parentKeys.key, hashKey: parentKeys.hashKey },
311
- address,
307
+ signingKeys,
312
308
  folderName,
313
309
  extendedAttributes,
314
310
  );
@@ -344,8 +340,8 @@ export class NodesManagement {
344
340
 
345
341
  // Decrypted metadata
346
342
  isStale: false,
347
- keyAuthor: resultOk(encryptedCrypto.signatureEmail),
348
- nameAuthor: resultOk(encryptedCrypto.signatureEmail),
343
+ keyAuthor: resultOk(encryptedCrypto.signatureEmail || null),
344
+ nameAuthor: resultOk(encryptedCrypto.signatureEmail || null),
349
345
  name: resultOk(folderName),
350
346
  treeEventScopeId: splitNodeUid(nodeUid).volumeId,
351
347
  };
@@ -1,5 +1,5 @@
1
1
  import { DriveCrypto } from '../../crypto';
2
- import { ProtonDriveTelemetry, UploadMetadata, Thumbnail } from '../../interface';
2
+ import { ProtonDriveTelemetry, UploadMetadata, Thumbnail, AnonymousUser } from '../../interface';
3
3
  import { DriveAPIService, drivePaths } from '../apiService';
4
4
  import { generateFileExtendedAttributes } from '../nodes';
5
5
  import { splitNodeRevisionUid } from '../uids';
@@ -198,7 +198,7 @@ export class PhotoUploadAPIService extends UploadAPIService {
198
198
  draftNodeRevisionUid: string,
199
199
  options: {
200
200
  armoredManifestSignature: string;
201
- signatureEmail: string;
201
+ signatureEmail: string | AnonymousUser;
202
202
  armoredExtendedAttributes?: string;
203
203
  },
204
204
  photo: {
@@ -220,7 +220,7 @@ export class PhotoUploadAPIService extends UploadAPIService {
220
220
  XAttr: options.armoredExtendedAttributes || null,
221
221
  Photo: {
222
222
  ContentHash: photo.contentHash,
223
- CaptureTime: photo.captureTime?.getTime() || 0,
223
+ CaptureTime: photo.captureTime ? Math.floor(photo.captureTime?.getTime() / 1000) : 0,
224
224
  MainPhotoLinkID: photo.mainPhotoLinkID || null,
225
225
  Tags: photo.tags || [],
226
226
  Exif: null, // Deprecated field, not used.
@@ -10,13 +10,13 @@ import { NodeAPIService } from '../nodes/apiService';
10
10
  import { NodesCache } from '../nodes/cache';
11
11
  import { NodesCryptoCache } from '../nodes/cryptoCache';
12
12
  import { NodesCryptoService } from '../nodes/cryptoService';
13
- import { NodesManagement } from '../nodes/nodesManagement';
14
13
  import { NodesRevisons } from '../nodes/nodesRevisions';
15
14
  import { SharingPublicCryptoReporter } from './cryptoReporter';
16
- import { SharingPublicNodesAccess } from './nodes';
15
+ import { SharingPublicNodesAccess, SharingPublicNodesManagement } from './nodes';
17
16
  import { SharingPublicSharesManager } from './shares';
18
17
 
19
18
  export { SharingPublicSessionManager } from './session/manager';
19
+ export { UnauthDriveAPIService } from './unauthApiService';
20
20
 
21
21
  /**
22
22
  * Provides facade for the whole sharing public module.
@@ -38,6 +38,7 @@ export function initSharingPublicModule(
38
38
  token: string,
39
39
  publicShareKey: PrivateKey,
40
40
  publicRootNodeUid: string,
41
+ isAnonymousContext: boolean,
41
42
  ) {
42
43
  const shares = new SharingPublicSharesManager(account, publicShareKey, publicRootNodeUid);
43
44
  const nodes = initSharingPublicNodesModule(
@@ -52,6 +53,7 @@ export function initSharingPublicModule(
52
53
  token,
53
54
  publicShareKey,
54
55
  publicRootNodeUid,
56
+ isAnonymousContext,
55
57
  );
56
58
 
57
59
  return {
@@ -78,6 +80,7 @@ export function initSharingPublicNodesModule(
78
80
  token: string,
79
81
  publicShareKey: PrivateKey,
80
82
  publicRootNodeUid: string,
83
+ isAnonymousContext: boolean,
81
84
  ) {
82
85
  const clientUid = undefined; // No client UID for public context yet.
83
86
  const api = new NodeAPIService(telemetry.getLogger('nodes-api'), apiService, clientUid);
@@ -96,8 +99,9 @@ export function initSharingPublicNodesModule(
96
99
  token,
97
100
  publicShareKey,
98
101
  publicRootNodeUid,
102
+ isAnonymousContext,
99
103
  );
100
- const nodesManagement = new NodesManagement(api, cryptoCache, cryptoService, nodesAccess);
104
+ const nodesManagement = new SharingPublicNodesManagement(api, cryptoCache, cryptoService, nodesAccess);
101
105
  const nodesRevisions = new NodesRevisons(telemetry.getLogger('nodes'), api, cryptoService, nodesAccess);
102
106
 
103
107
  return {
@@ -1,13 +1,14 @@
1
- import { ProtonDriveTelemetry } from '../../interface';
1
+ import { NodeResult, ProtonDriveTelemetry } from '../../interface';
2
2
  import { NodeAPIService } from '../nodes/apiService';
3
3
  import { NodesCache } from '../nodes/cache';
4
4
  import { NodesCryptoCache } from '../nodes/cryptoCache';
5
5
  import { NodesCryptoService } from '../nodes/cryptoService';
6
6
  import { NodesAccess } from '../nodes/nodesAccess';
7
+ import { NodesManagement } from '../nodes/nodesManagement';
7
8
  import { isProtonDocument, isProtonSheet } from '../nodes/mediaTypes';
8
9
  import { splitNodeUid } from '../uids';
9
10
  import { SharingPublicSharesManager } from './shares';
10
- import { DecryptedNode, DecryptedNodeKeys } from '../nodes/interface';
11
+ import { DecryptedNode, DecryptedNodeKeys, NodeSigningKeys } from '../nodes/interface';
11
12
  import { PrivateKey } from '../../crypto';
12
13
 
13
14
  export class SharingPublicNodesAccess extends NodesAccess {
@@ -22,11 +23,13 @@ export class SharingPublicNodesAccess extends NodesAccess {
22
23
  private token: string,
23
24
  private publicShareKey: PrivateKey,
24
25
  private publicRootNodeUid: string,
26
+ private isAnonymousContext: boolean,
25
27
  ) {
26
28
  super(telemetry, apiService, cache, cryptoCache, cryptoService, sharesService);
27
29
  this.token = token;
28
30
  this.publicShareKey = publicShareKey;
29
31
  this.publicRootNodeUid = publicRootNodeUid;
32
+ this.isAnonymousContext = isAnonymousContext;
30
33
  }
31
34
 
32
35
  async getParentKeys(
@@ -56,4 +59,42 @@ export class SharingPublicNodesAccess extends NodesAccess {
56
59
  // Public link doesn't support specific node URLs.
57
60
  return this.url;
58
61
  }
62
+
63
+ async getNodeSigningKeys(
64
+ uids: { nodeUid: string; parentNodeUid?: string } | { nodeUid?: string; parentNodeUid: string },
65
+ ): Promise<NodeSigningKeys> {
66
+ if (this.isAnonymousContext) {
67
+ const nodeKeys = uids.nodeUid ? await this.getNodeKeys(uids.nodeUid) : { key: undefined };
68
+ const parentNodeKeys = uids.parentNodeUid ? await this.getNodeKeys(uids.parentNodeUid) : { key: undefined };
69
+ return {
70
+ type: 'nodeKey',
71
+ nodeKey: nodeKeys.key,
72
+ parentNodeKey: parentNodeKeys.key,
73
+ };
74
+ }
75
+
76
+ return super.getNodeSigningKeys(uids);
77
+ }
78
+ }
79
+
80
+ export class SharingPublicNodesManagement extends NodesManagement {
81
+ constructor(
82
+ apiService: NodeAPIService,
83
+ cryptoCache: NodesCryptoCache,
84
+ cryptoService: NodesCryptoService,
85
+ nodesAccess: SharingPublicNodesAccess,
86
+ ) {
87
+ super(apiService, cryptoCache, cryptoService, nodesAccess);
88
+ }
89
+
90
+ async *deleteNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
91
+ // Public link does not support trashing and deleting trashed nodes.
92
+ // Instead, if user is owner, API allows directly deleting existing nodes.
93
+ for await (const result of this.apiService.deleteExistingNodes(nodeUids, signal)) {
94
+ if (result.ok) {
95
+ await this.nodesAccess.notifyNodeDeleted(result.uid);
96
+ }
97
+ yield result;
98
+ }
99
+ }
59
100
  }
@@ -0,0 +1,29 @@
1
+ import { getUnauthEndpoint } from './unauthApiService';
2
+
3
+ describe('getUnauthEndpoint', () => {
4
+ it('should not change urls endpoints', () => {
5
+ expect(getUnauthEndpoint('drive/urls/anything')).toBe('drive/urls/anything');
6
+ expect(getUnauthEndpoint('drive/urls/drive/anything')).toBe('drive/urls/drive/anything');
7
+ expect(getUnauthEndpoint('drive/urls/drive/v2/anything')).toBe('drive/urls/drive/v2/anything');
8
+ });
9
+
10
+ it('should not change v2/urls endpoints', () => {
11
+ expect(getUnauthEndpoint('drive/v2/urls/anything')).toBe('drive/v2/urls/anything');
12
+ expect(getUnauthEndpoint('drive/v2/urls/drive/anything')).toBe('drive/v2/urls/drive/anything');
13
+ expect(getUnauthEndpoint('drive/v2/urls/drive/v2/anything')).toBe('drive/v2/urls/drive/v2/anything');
14
+ });
15
+
16
+ it('should put unauth prefix for v2 endpoints', () => {
17
+ expect(getUnauthEndpoint('drive/v2/anything')).toBe('drive/unauth/v2/anything');
18
+ expect(getUnauthEndpoint('drive/v2/drive/anything')).toBe('drive/unauth/v2/drive/anything');
19
+ expect(getUnauthEndpoint('drive/v2/drive/v2/anything')).toBe('drive/unauth/v2/drive/v2/anything');
20
+ });
21
+
22
+ it('should put unauth prefix for non-v2 endpoints', () => {
23
+ expect(getUnauthEndpoint('drive/anything')).toBe('drive/unauth/anything');
24
+ expect(getUnauthEndpoint('drive/anything/v2/anything')).toBe('drive/unauth/anything/v2/anything');
25
+ expect(getUnauthEndpoint('drive/anything/drive/anything')).toBe('drive/unauth/anything/drive/anything');
26
+ expect(getUnauthEndpoint('drive/anything/drive/v2/anything')).toBe('drive/unauth/anything/drive/v2/anything');
27
+ });
28
+ });
29
+
@@ -0,0 +1,32 @@
1
+ import { DriveAPIService } from '../apiService';
2
+
3
+ /**
4
+ * Drive API Service for public links.
5
+ *
6
+ * This service is used to make requests to the Drive API without
7
+ * authentication. The unauth context uses the same endpoint, but
8
+ * with an `unauth` prefix. The goal is to avoid the need to use
9
+ * different path and use the exact endpoint for both contexts.
10
+ * However, API has global logic for handling expired sessions that
11
+ * is not compatible with the unauth context. For this reason, this
12
+ * service is used to make requests to the Drive API for public
13
+ * link context in the mean time.
14
+ */
15
+ export class UnauthDriveAPIService extends DriveAPIService {
16
+ protected async makeRequest<RequestPayload, ResponsePayload>(
17
+ url: string,
18
+ method = 'GET',
19
+ data?: RequestPayload,
20
+ signal?: AbortSignal,
21
+ ): Promise<ResponsePayload> {
22
+ const unauthUrl = getUnauthEndpoint(url);
23
+ return super.makeRequest(unauthUrl, method, data, signal);
24
+ }
25
+ }
26
+
27
+ export function getUnauthEndpoint(url: string): string {
28
+ if (url.startsWith('drive/urls/') || url.startsWith('drive/v2/urls/')) {
29
+ return url;
30
+ }
31
+ return url.replace(/^drive\//, 'drive/unauth/');
32
+ }
@@ -1,6 +1,7 @@
1
1
  import { c } from 'ttag';
2
2
 
3
3
  import { base64StringToUint8Array, uint8ArrayToBase64String } from '../../crypto';
4
+ import { AnonymousUser } from '../../interface';
4
5
  import { APICodeError, DriveAPIService, drivePaths, isCodeOk } from '../apiService';
5
6
  import { splitNodeUid, makeNodeUid, splitNodeRevisionUid, makeNodeRevisionUid } from '../uids';
6
7
  import { UploadTokens } from './interface';
@@ -65,7 +66,7 @@ export class UploadAPIService {
65
66
  armoredNodePassphraseSignature: string;
66
67
  base64ContentKeyPacket: string;
67
68
  armoredContentKeyPacketSignature: string;
68
- signatureEmail: string;
69
+ signatureEmail: string | AnonymousUser;
69
70
  },
70
71
  ): Promise<{
71
72
  nodeUid: string;
@@ -150,7 +151,7 @@ export class UploadAPIService {
150
151
 
151
152
  async requestBlockUpload(
152
153
  draftNodeRevisionUid: string,
153
- addressId: string,
154
+ addressId: string | AnonymousUser,
154
155
  blocks: {
155
156
  contentBlocks: {
156
157
  index: number;
@@ -211,7 +212,7 @@ export class UploadAPIService {
211
212
  draftNodeRevisionUid: string,
212
213
  options: {
213
214
  armoredManifestSignature: string;
214
- signatureEmail: string;
215
+ signatureEmail: string | AnonymousUser;
215
216
  armoredExtendedAttributes?: string;
216
217
  },
217
218
  ): Promise<void> {