@protontech/drive-sdk 0.1.2 → 0.2.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 (153) hide show
  1. package/dist/crypto/driveCrypto.d.ts +11 -0
  2. package/dist/crypto/driveCrypto.js +20 -7
  3. package/dist/crypto/driveCrypto.js.map +1 -1
  4. package/dist/crypto/interface.d.ts +10 -1
  5. package/dist/crypto/openPGPCrypto.d.ts +18 -2
  6. package/dist/crypto/openPGPCrypto.js +25 -6
  7. package/dist/crypto/openPGPCrypto.js.map +1 -1
  8. package/dist/diagnostic/telemetry.d.ts +1 -1
  9. package/dist/diagnostic/telemetry.js +1 -1
  10. package/dist/diagnostic/telemetry.js.map +1 -1
  11. package/dist/interface/download.d.ts +46 -0
  12. package/dist/interface/index.d.ts +2 -2
  13. package/dist/interface/index.js.map +1 -1
  14. package/dist/interface/nodes.d.ts +26 -1
  15. package/dist/interface/nodes.js.map +1 -1
  16. package/dist/interface/telemetry.d.ts +5 -2
  17. package/dist/interface/telemetry.js.map +1 -1
  18. package/dist/internal/apiService/apiService.js +1 -1
  19. package/dist/internal/apiService/apiService.js.map +1 -1
  20. package/dist/internal/apiService/driveTypes.d.ts +78 -165
  21. package/dist/internal/apiService/index.d.ts +1 -1
  22. package/dist/internal/apiService/index.js +2 -2
  23. package/dist/internal/apiService/index.js.map +1 -1
  24. package/dist/internal/apiService/transformers.d.ts +1 -1
  25. package/dist/internal/apiService/transformers.js +2 -2
  26. package/dist/internal/apiService/transformers.js.map +1 -1
  27. package/dist/internal/download/blockIndex.d.ts +11 -0
  28. package/dist/internal/download/blockIndex.js +35 -0
  29. package/dist/internal/download/blockIndex.js.map +1 -0
  30. package/dist/internal/download/blockIndex.test.d.ts +1 -0
  31. package/dist/internal/download/blockIndex.test.js +147 -0
  32. package/dist/internal/download/blockIndex.test.js.map +1 -0
  33. package/dist/internal/download/fileDownloader.d.ts +6 -2
  34. package/dist/internal/download/fileDownloader.js +83 -6
  35. package/dist/internal/download/fileDownloader.js.map +1 -1
  36. package/dist/internal/download/fileDownloader.test.js +69 -4
  37. package/dist/internal/download/fileDownloader.test.js.map +1 -1
  38. package/dist/internal/download/interface.d.ts +4 -4
  39. package/dist/internal/download/seekableStream.d.ts +80 -0
  40. package/dist/internal/download/seekableStream.js +163 -0
  41. package/dist/internal/download/seekableStream.js.map +1 -0
  42. package/dist/internal/download/seekableStream.test.d.ts +1 -0
  43. package/dist/internal/download/seekableStream.test.js +149 -0
  44. package/dist/internal/download/seekableStream.test.js.map +1 -0
  45. package/dist/internal/download/telemetry.js +1 -1
  46. package/dist/internal/download/telemetry.js.map +1 -1
  47. package/dist/internal/download/telemetry.test.js +7 -7
  48. package/dist/internal/download/telemetry.test.js.map +1 -1
  49. package/dist/internal/errors.d.ts +1 -1
  50. package/dist/internal/errors.js +7 -1
  51. package/dist/internal/errors.js.map +1 -1
  52. package/dist/internal/errors.test.js +44 -10
  53. package/dist/internal/errors.test.js.map +1 -1
  54. package/dist/internal/events/index.js +1 -1
  55. package/dist/internal/events/index.js.map +1 -1
  56. package/dist/internal/nodes/apiService.js +16 -3
  57. package/dist/internal/nodes/apiService.js.map +1 -1
  58. package/dist/internal/nodes/apiService.test.js +43 -7
  59. package/dist/internal/nodes/apiService.test.js.map +1 -1
  60. package/dist/internal/nodes/cache.js +9 -2
  61. package/dist/internal/nodes/cache.js.map +1 -1
  62. package/dist/internal/nodes/cache.test.js +6 -1
  63. package/dist/internal/nodes/cache.test.js.map +1 -1
  64. package/dist/internal/nodes/cryptoService.d.ts +4 -1
  65. package/dist/internal/nodes/cryptoService.js +66 -16
  66. package/dist/internal/nodes/cryptoService.js.map +1 -1
  67. package/dist/internal/nodes/cryptoService.test.js +129 -46
  68. package/dist/internal/nodes/cryptoService.test.js.map +1 -1
  69. package/dist/internal/nodes/extendedAttributes.d.ts +2 -1
  70. package/dist/internal/nodes/extendedAttributes.js +27 -1
  71. package/dist/internal/nodes/extendedAttributes.js.map +1 -1
  72. package/dist/internal/nodes/extendedAttributes.test.js +59 -6
  73. package/dist/internal/nodes/extendedAttributes.test.js.map +1 -1
  74. package/dist/internal/nodes/index.test.js +1 -1
  75. package/dist/internal/nodes/index.test.js.map +1 -1
  76. package/dist/internal/nodes/interface.d.ts +18 -2
  77. package/dist/internal/nodes/nodesAccess.js +11 -1
  78. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  79. package/dist/internal/nodes/nodesManagement.js +1 -1
  80. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  81. package/dist/internal/nodes/nodesRevisions.d.ts +4 -3
  82. package/dist/internal/nodes/nodesRevisions.js +2 -2
  83. package/dist/internal/nodes/nodesRevisions.js.map +1 -1
  84. package/dist/internal/shares/cryptoService.js +7 -4
  85. package/dist/internal/shares/cryptoService.js.map +1 -1
  86. package/dist/internal/shares/cryptoService.test.js +5 -3
  87. package/dist/internal/shares/cryptoService.test.js.map +1 -1
  88. package/dist/internal/sharing/apiService.js +5 -5
  89. package/dist/internal/sharing/apiService.js.map +1 -1
  90. package/dist/internal/sharing/cryptoService.js +8 -5
  91. package/dist/internal/sharing/cryptoService.js.map +1 -1
  92. package/dist/internal/sharing/cryptoService.test.js +7 -4
  93. package/dist/internal/sharing/cryptoService.test.js.map +1 -1
  94. package/dist/internal/upload/telemetry.js +2 -2
  95. package/dist/internal/upload/telemetry.js.map +1 -1
  96. package/dist/internal/upload/telemetry.test.js +7 -7
  97. package/dist/internal/upload/telemetry.test.js.map +1 -1
  98. package/dist/telemetry.d.ts +2 -2
  99. package/dist/telemetry.js +2 -2
  100. package/dist/telemetry.js.map +1 -1
  101. package/dist/tests/telemetry.js +1 -1
  102. package/dist/tests/telemetry.js.map +1 -1
  103. package/dist/transformers.d.ts +1 -1
  104. package/dist/transformers.js +2 -1
  105. package/dist/transformers.js.map +1 -1
  106. package/package.json +1 -1
  107. package/src/crypto/driveCrypto.ts +70 -25
  108. package/src/crypto/interface.ts +15 -0
  109. package/src/crypto/openPGPCrypto.ts +37 -5
  110. package/src/diagnostic/telemetry.ts +1 -1
  111. package/src/interface/download.ts +46 -0
  112. package/src/interface/index.ts +2 -1
  113. package/src/interface/nodes.ts +28 -1
  114. package/src/interface/telemetry.ts +6 -1
  115. package/src/internal/apiService/apiService.ts +1 -1
  116. package/src/internal/apiService/driveTypes.ts +78 -165
  117. package/src/internal/apiService/index.ts +1 -1
  118. package/src/internal/apiService/transformers.ts +1 -1
  119. package/src/internal/download/blockIndex.test.ts +158 -0
  120. package/src/internal/download/blockIndex.ts +36 -0
  121. package/src/internal/download/fileDownloader.test.ts +100 -7
  122. package/src/internal/download/fileDownloader.ts +109 -9
  123. package/src/internal/download/interface.ts +4 -4
  124. package/src/internal/download/seekableStream.test.ts +187 -0
  125. package/src/internal/download/seekableStream.ts +182 -0
  126. package/src/internal/download/telemetry.test.ts +7 -7
  127. package/src/internal/download/telemetry.ts +1 -1
  128. package/src/internal/errors.test.ts +45 -11
  129. package/src/internal/errors.ts +8 -0
  130. package/src/internal/events/index.ts +1 -1
  131. package/src/internal/nodes/apiService.test.ts +59 -15
  132. package/src/internal/nodes/apiService.ts +21 -4
  133. package/src/internal/nodes/cache.test.ts +6 -1
  134. package/src/internal/nodes/cache.ts +9 -2
  135. package/src/internal/nodes/cryptoService.test.ts +139 -47
  136. package/src/internal/nodes/cryptoService.ts +94 -9
  137. package/src/internal/nodes/extendedAttributes.test.ts +60 -7
  138. package/src/internal/nodes/extendedAttributes.ts +37 -1
  139. package/src/internal/nodes/index.test.ts +1 -1
  140. package/src/internal/nodes/interface.ts +19 -2
  141. package/src/internal/nodes/nodesAccess.ts +15 -1
  142. package/src/internal/nodes/nodesManagement.ts +1 -1
  143. package/src/internal/nodes/nodesRevisions.ts +14 -5
  144. package/src/internal/shares/cryptoService.test.ts +5 -3
  145. package/src/internal/shares/cryptoService.ts +7 -4
  146. package/src/internal/sharing/apiService.ts +6 -6
  147. package/src/internal/sharing/cryptoService.test.ts +7 -4
  148. package/src/internal/sharing/cryptoService.ts +8 -5
  149. package/src/internal/upload/telemetry.test.ts +7 -7
  150. package/src/internal/upload/telemetry.ts +2 -2
  151. package/src/telemetry.ts +2 -2
  152. package/src/tests/telemetry.ts +1 -1
  153. package/src/transformers.ts +4 -2
@@ -83,83 +83,100 @@ describe('extended attrbiutes', () => {
83
83
  });
84
84
 
85
85
  describe('should parses file attributes', () => {
86
- const testCases: [string, FileExtendedAttributesParsed][] = [
87
- ['', {}],
88
- ['{}', {}],
89
- ['a', {}],
86
+ const testCases: [Date, string, FileExtendedAttributesParsed][] = [
87
+ [new Date('2025-01-01'), '', {}],
88
+ [new Date('2025-01-01'), '{}', {}],
89
+ [new Date('2025-01-01'), 'a', {}],
90
90
  [
91
+ new Date('2025-01-01'),
91
92
  '{"Common": {"ModificationTime": "2009-02-13T23:31:30+0000"}}',
92
93
  {
93
94
  claimedModificationTime: new Date(1234567890000),
94
95
  claimedSize: undefined,
95
96
  claimedDigests: undefined,
96
97
  claimedAdditionalMetadata: undefined,
98
+ claimedBlockSizes: undefined,
97
99
  },
98
100
  ],
99
101
  [
102
+ new Date('2025-01-01'),
100
103
  '{"Common": {"Size": 123}}',
101
104
  {
102
105
  claimedModificationTime: undefined,
103
106
  claimedSize: 123,
104
107
  claimedDigests: undefined,
105
108
  claimedAdditionalMetadata: undefined,
109
+ claimedBlockSizes: undefined,
106
110
  },
107
111
  ],
108
112
  [
109
- '{"Common": {"ModificationTime": "2009-02-13T23:31:30+0000", "Size": 123, "BlockSizes": [1, 2, 3]}}',
113
+ new Date('2025-01-01'),
114
+ '{"Common": {"ModificationTime": "2009-02-13T23:31:30+0000", "Size": 123, "BlockSizes": [123]}}',
110
115
  {
111
116
  claimedModificationTime: new Date(1234567890000),
112
117
  claimedSize: 123,
113
118
  claimedDigests: undefined,
114
119
  claimedAdditionalMetadata: undefined,
120
+ claimedBlockSizes: [123],
115
121
  },
116
122
  ],
117
123
  [
124
+ new Date('2025-01-01'),
118
125
  '{"Common": {"ModificationTime": "aa", "Size": 123}}',
119
126
  {
120
127
  claimedModificationTime: undefined,
121
128
  claimedSize: 123,
122
129
  claimedDigests: undefined,
123
130
  claimedAdditionalMetadata: undefined,
131
+ claimedBlockSizes: undefined,
124
132
  },
125
133
  ],
126
134
  [
135
+ new Date('2025-01-01'),
127
136
  '{"Common": {"ModificationTime": "2009-02-13T23:31:30+0000", "Size": "aaa"}}',
128
137
  {
129
138
  claimedModificationTime: new Date(1234567890000),
130
139
  claimedSize: undefined,
131
140
  claimedDigests: undefined,
132
141
  claimedAdditionalMetadata: undefined,
142
+ claimedBlockSizes: undefined,
133
143
  },
134
144
  ],
135
145
  [
146
+ new Date('2025-01-01'),
136
147
  '{"Common": {"Digests": {}}}',
137
148
  {
138
149
  claimedModificationTime: undefined,
139
150
  claimedSize: undefined,
140
151
  claimedDigests: undefined,
141
152
  claimedAdditionalMetadata: undefined,
153
+ claimedBlockSizes: undefined,
142
154
  },
143
155
  ],
144
156
  [
157
+ new Date('2025-01-01'),
145
158
  '{"Common": {"Digests": {"SHA1": null}}}',
146
159
  {
147
160
  claimedModificationTime: undefined,
148
161
  claimedSize: undefined,
149
162
  claimedDigests: undefined,
150
163
  claimedAdditionalMetadata: undefined,
164
+ claimedBlockSizes: undefined,
151
165
  },
152
166
  ],
153
167
  [
168
+ new Date('2025-01-01'),
154
169
  '{"Common": {"Digests": {"SHA1": "abcdef"}}}',
155
170
  {
156
171
  claimedModificationTime: undefined,
157
172
  claimedSize: undefined,
158
173
  claimedDigests: { sha1: 'abcdef' },
159
174
  claimedAdditionalMetadata: undefined,
175
+ claimedBlockSizes: undefined,
160
176
  },
161
177
  ],
162
178
  [
179
+ new Date('2025-01-01'),
163
180
  '{"Common": {}, "Media": {}}',
164
181
  {
165
182
  claimedModificationTime: undefined,
@@ -168,12 +185,48 @@ describe('extended attrbiutes', () => {
168
185
  claimedAdditionalMetadata: {
169
186
  Media: {},
170
187
  },
188
+ claimedBlockSizes: undefined,
189
+ },
190
+ ],
191
+ [
192
+ new Date('2025-01-01'),
193
+ '{"Common": {"BlockSizes": [1024, 1024, 1024, 1024, 123]}}',
194
+ {
195
+ claimedModificationTime: undefined,
196
+ claimedSize: undefined,
197
+ claimedDigests: undefined,
198
+ claimedAdditionalMetadata: undefined,
199
+ claimedBlockSizes: [1024, 1024, 1024, 1024, 123],
200
+ },
201
+ ],
202
+ [
203
+ // Starting from 2025-01-01, block sizes are passed as is.
204
+ new Date('2025-01-01'),
205
+ '{"Common": {"BlockSizes": [1024, 1024, 123, 1024, 1024]}}',
206
+ {
207
+ claimedModificationTime: undefined,
208
+ claimedSize: undefined,
209
+ claimedDigests: undefined,
210
+ claimedAdditionalMetadata: undefined,
211
+ claimedBlockSizes: [1024, 1024, 123, 1024, 1024],
212
+ },
213
+ ],
214
+ [
215
+ // Before 2025-01-01, block sizes are sorted in descending order.
216
+ new Date('2024-01-01'),
217
+ '{"Common": {"BlockSizes": [123, 1024, 1024, 1024, 1024]}}',
218
+ {
219
+ claimedModificationTime: undefined,
220
+ claimedSize: undefined,
221
+ claimedDigests: undefined,
222
+ claimedAdditionalMetadata: undefined,
223
+ claimedBlockSizes: [1024, 1024, 1024, 1024, 123],
171
224
  },
172
225
  ],
173
226
  ];
174
- testCases.forEach(([input, expectedAttributes]) => {
227
+ testCases.forEach(([creationTime, input, expectedAttributes]) => {
175
228
  it(`should parse ${input}`, () => {
176
- const output = parseFileExtendedAttributes(getMockLogger(), input);
229
+ const output = parseFileExtendedAttributes(getMockLogger(), creationTime, input);
177
230
  expect(output).toMatchObject(expectedAttributes);
178
231
  });
179
232
  });
@@ -48,6 +48,7 @@ export interface FileExtendedAttributesParsed {
48
48
  sha1?: string;
49
49
  };
50
50
  claimedAdditionalMetadata?: object;
51
+ claimedBlockSizes?: number[];
51
52
  }
52
53
 
53
54
  export function generateFolderExtendedAttributes(claimedModificationTime?: Date): string | undefined {
@@ -113,7 +114,11 @@ export function generateFileExtendedAttributes(options: {
113
114
  });
114
115
  }
115
116
 
116
- export function parseFileExtendedAttributes(logger: Logger, extendedAttributes?: string): FileExtendedAttributesParsed {
117
+ export function parseFileExtendedAttributes(
118
+ logger: Logger,
119
+ creationTime: Date,
120
+ extendedAttributes?: string,
121
+ ): FileExtendedAttributesParsed {
117
122
  if (!extendedAttributes) {
118
123
  return {};
119
124
  }
@@ -131,6 +136,7 @@ export function parseFileExtendedAttributes(logger: Logger, extendedAttributes?:
131
136
  claimedAdditionalMetadata: Object.keys(claimedAdditionalMetadata).length
132
137
  ? claimedAdditionalMetadata
133
138
  : undefined,
139
+ claimedBlockSizes: parseBlockSizes(logger, creationTime, parsed),
134
140
  };
135
141
  } catch (error: unknown) {
136
142
  logger.error(`Failed to parse extended attributes`, error);
@@ -183,3 +189,33 @@ function parseDigests(logger: Logger, xattr?: FileExtendedAttributesSchema): { s
183
189
  sha1,
184
190
  };
185
191
  }
192
+
193
+ function parseBlockSizes(
194
+ logger: Logger,
195
+ creationTime: Date,
196
+ xattr?: FileExtendedAttributesSchema,
197
+ ): number[] | undefined {
198
+ const blockSizes = xattr?.Common?.BlockSizes;
199
+ if (blockSizes === undefined) {
200
+ return undefined;
201
+ }
202
+ if (!Array.isArray(blockSizes)) {
203
+ logger.warn(`XAttr block sizes "${blockSizes}" is not valid`);
204
+ return undefined;
205
+ }
206
+ if (blockSizes.some((size) => typeof size !== 'number' || size <= 0)) {
207
+ logger.warn(`XAttr block sizes "${blockSizes}" is not valid`);
208
+ return undefined;
209
+ }
210
+ if (blockSizes.length === 0) {
211
+ return undefined;
212
+ }
213
+ // Before 2025, there was a bug on the Windows client that didn't sort
214
+ // the block sizes in correct order. Because the sizes were all the same
215
+ // except the last one, which was always smaller, the block sizes must be
216
+ // sorted in descending order.
217
+ if (creationTime < new Date('2025-01-01')) {
218
+ return blockSizes.sort((a, b) => b - a);
219
+ }
220
+ return blockSizes;
221
+ }
@@ -20,7 +20,7 @@ function generateNode(uid: string, parentUid = 'volumeId~root', params: Partial<
20
20
  return {
21
21
  uid,
22
22
  parentUid,
23
- directMemberRole: MemberRole.Admin,
23
+ directRole: MemberRole.Admin,
24
24
  type: NodeType.File,
25
25
  mediaType: 'text',
26
26
  isShared: false,
@@ -32,7 +32,12 @@ interface BaseNode {
32
32
  // Share node metadata
33
33
  shareId?: string;
34
34
  isShared: boolean;
35
- directMemberRole: MemberRole;
35
+ directRole: MemberRole;
36
+ membership?: {
37
+ role: MemberRole;
38
+ inviteTime: Date;
39
+ // TODO: acceptedBy: Author;
40
+ };
36
41
  }
37
42
 
38
43
  /**
@@ -50,6 +55,12 @@ export interface EncryptedNodeCrypto {
50
55
  armoredKey: string;
51
56
  armoredNodePassphrase: string;
52
57
  armoredNodePassphraseSignature: string;
58
+ membership?: {
59
+ inviterEmail: string;
60
+ base64MemberSharePassphraseKeyPacket: string;
61
+ armoredInviterSharePassphraseKeyPacketSignature: string;
62
+ armoredInviteeSharePassphraseSessionKeySignature: string;
63
+ };
53
64
  }
54
65
 
55
66
  export interface EncryptedNodeFileCrypto extends EncryptedNodeCrypto {
@@ -78,9 +89,14 @@ export interface EncryptedNodeAlbumCrypto extends EncryptedNodeCrypto {}
78
89
  * This interface is holding decrypted node metadata that is not yet parsed,
79
90
  * such as extended attributes.
80
91
  */
81
- export interface DecryptedUnparsedNode extends BaseNode {
92
+ export interface DecryptedUnparsedNode extends Omit<BaseNode, 'membership'> {
82
93
  keyAuthor: Author;
83
94
  nameAuthor: Author;
95
+ membership?: {
96
+ role: MemberRole;
97
+ inviteTime: Date;
98
+ sharedBy: Author;
99
+ };
84
100
  name: Result<string, Error>;
85
101
  activeRevision?: Result<DecryptedUnparsedRevision, Error>;
86
102
  folder?: {
@@ -147,6 +163,7 @@ export interface DecryptedUnparsedRevision extends BaseRevision {
147
163
 
148
164
  export interface DecryptedRevision extends Revision {
149
165
  thumbnails?: Thumbnail[];
166
+ claimedBlockSizes?: number[];
150
167
  }
151
168
 
152
169
  /**
@@ -270,6 +270,16 @@ export class NodesAccess {
270
270
  claimedAuthor: encryptedNode.encryptedCrypto.nameSignatureEmail,
271
271
  error: getErrorMessage(error),
272
272
  }),
273
+ membership: encryptedNode.membership
274
+ ? {
275
+ role: encryptedNode.membership.role,
276
+ inviteTime: encryptedNode.membership.inviteTime,
277
+ sharedBy: resultError({
278
+ claimedAuthor: encryptedNode.encryptedCrypto.membership?.inviterEmail,
279
+ error: getErrorMessage(error),
280
+ }),
281
+ }
282
+ : undefined,
273
283
  errors: [error],
274
284
  treeEventScopeId: splitNodeUid(encryptedNode.uid).volumeId,
275
285
  },
@@ -311,7 +321,11 @@ export class NodesAccess {
311
321
 
312
322
  if (unparsedNode.type === NodeType.File) {
313
323
  const extendedAttributes = unparsedNode.activeRevision?.ok
314
- ? parseFileExtendedAttributes(this.logger, unparsedNode.activeRevision.value.extendedAttributes)
324
+ ? parseFileExtendedAttributes(
325
+ this.logger,
326
+ unparsedNode.activeRevision.value.creationTime,
327
+ unparsedNode.activeRevision.value.extendedAttributes,
328
+ )
315
329
  : undefined;
316
330
 
317
331
  return {
@@ -245,7 +245,7 @@ export class NodesManagement {
245
245
 
246
246
  // Share node metadata
247
247
  isShared: false,
248
- directMemberRole: MemberRole.Inherited,
248
+ directRole: MemberRole.Inherited,
249
249
 
250
250
  // Decrypted metadata
251
251
  isStale: false,
@@ -1,9 +1,10 @@
1
- import { Logger, Revision } from '../../interface';
1
+ import { Logger } from '../../interface';
2
2
  import { makeNodeUidFromRevisionUid } from '../uids';
3
3
  import { NodeAPIService } from './apiService';
4
4
  import { NodesCryptoService } from './cryptoService';
5
5
  import { NodesAccess } from './nodesAccess';
6
6
  import { parseFileExtendedAttributes } from './extendedAttributes';
7
+ import { DecryptedRevision } from './interface';
7
8
 
8
9
  /**
9
10
  * Provides access to revisions metadata.
@@ -21,26 +22,34 @@ export class NodesRevisons {
21
22
  this.nodesAccess = nodesAccess;
22
23
  }
23
24
 
24
- async getRevision(nodeRevisionUid: string): Promise<Revision> {
25
+ async getRevision(nodeRevisionUid: string): Promise<DecryptedRevision> {
25
26
  const nodeUid = makeNodeUidFromRevisionUid(nodeRevisionUid);
26
27
  const { key } = await this.nodesAccess.getNodeKeys(nodeUid);
27
28
 
28
29
  const encryptedRevision = await this.apiService.getRevision(nodeRevisionUid);
29
30
  const revision = await this.cryptoService.decryptRevision(nodeUid, encryptedRevision, key);
30
- const extendedAttributes = parseFileExtendedAttributes(this.logger, revision.extendedAttributes);
31
+ const extendedAttributes = parseFileExtendedAttributes(
32
+ this.logger,
33
+ revision.creationTime,
34
+ revision.extendedAttributes,
35
+ );
31
36
  return {
32
37
  ...revision,
33
38
  ...extendedAttributes,
34
39
  };
35
40
  }
36
41
 
37
- async *iterateRevisions(nodeUid: string, signal?: AbortSignal): AsyncGenerator<Revision> {
42
+ async *iterateRevisions(nodeUid: string, signal?: AbortSignal): AsyncGenerator<DecryptedRevision> {
38
43
  const { key } = await this.nodesAccess.getNodeKeys(nodeUid);
39
44
 
40
45
  const encryptedRevisions = await this.apiService.getRevisions(nodeUid, signal);
41
46
  for (const encryptedRevision of encryptedRevisions) {
42
47
  const revision = await this.cryptoService.decryptRevision(nodeUid, encryptedRevision, key);
43
- const extendedAttributes = parseFileExtendedAttributes(this.logger, revision.extendedAttributes);
48
+ const extendedAttributes = parseFileExtendedAttributes(
49
+ this.logger,
50
+ revision.creationTime,
51
+ revision.extendedAttributes,
52
+ );
44
53
  yield {
45
54
  ...revision,
46
55
  ...extendedAttributes,
@@ -59,7 +59,7 @@ describe('SharesCryptoService', () => {
59
59
 
60
60
  expect(account.getOwnAddress).toHaveBeenCalledWith('addressId');
61
61
  expect(account.getPublicKeys).toHaveBeenCalledWith('signatureEmail');
62
- expect(telemetry.logEvent).not.toHaveBeenCalled();
62
+ expect(telemetry.recordMetric).not.toHaveBeenCalled();
63
63
  });
64
64
 
65
65
  it('should decrypt root share with signiture verification error', async () => {
@@ -97,12 +97,13 @@ describe('SharesCryptoService', () => {
97
97
 
98
98
  expect(account.getOwnAddress).toHaveBeenCalledWith('addressId');
99
99
  expect(account.getPublicKeys).toHaveBeenCalledWith('signatureEmail');
100
- expect(telemetry.logEvent).toHaveBeenCalledWith({
100
+ expect(telemetry.recordMetric).toHaveBeenCalledWith({
101
101
  eventName: 'verificationError',
102
102
  volumeType: 'own_volume',
103
103
  field: 'shareKey',
104
104
  addressMatchingDefaultShare: undefined,
105
105
  fromBefore2024: undefined,
106
+ uid: 'shareId',
106
107
  });
107
108
  });
108
109
 
@@ -124,12 +125,13 @@ describe('SharesCryptoService', () => {
124
125
 
125
126
  await expect(result).rejects.toThrow(error);
126
127
 
127
- expect(telemetry.logEvent).toHaveBeenCalledWith({
128
+ expect(telemetry.recordMetric).toHaveBeenCalledWith({
128
129
  eventName: 'decryptionError',
129
130
  volumeType: 'own_volume',
130
131
  field: 'shareKey',
131
132
  fromBefore2024: undefined,
132
133
  error,
134
+ uid: 'shareId',
133
135
  });
134
136
  });
135
137
  });
@@ -76,7 +76,7 @@ export class SharesCryptoService {
76
76
  const { keys: addressKeys } = await this.account.getOwnAddress(share.addressId);
77
77
  const addressPublicKeys = await this.account.getPublicKeys(share.creatorEmail);
78
78
 
79
- let key, passphraseSessionKey, verified;
79
+ let key, passphraseSessionKey, verified, verificationErrors;
80
80
  try {
81
81
  const result = await this.driveCrypto.decryptKey(
82
82
  share.encryptedCrypto.armoredKey,
@@ -88,6 +88,7 @@ export class SharesCryptoService {
88
88
  key = result.key;
89
89
  passphraseSessionKey = result.passphraseSessionKey;
90
90
  verified = result.verified;
91
+ verificationErrors = result.verificationErrors;
91
92
  } catch (error: unknown) {
92
93
  this.reportDecryptionError(share, error);
93
94
  throw error;
@@ -98,7 +99,7 @@ export class SharesCryptoService {
98
99
  ? resultOk(share.creatorEmail)
99
100
  : resultError({
100
101
  claimedAuthor: share.creatorEmail,
101
- error: getVerificationMessage(verified),
102
+ error: getVerificationMessage(verified, verificationErrors),
102
103
  });
103
104
 
104
105
  if (!author.ok) {
@@ -125,12 +126,13 @@ export class SharesCryptoService {
125
126
  const fromBefore2024 = share.creationTime ? share.creationTime < new Date('2024-01-01') : undefined;
126
127
  this.logger.error(`Failed to decrypt share ${share.shareId} (from before 2024: ${fromBefore2024})`, error);
127
128
 
128
- this.telemetry.logEvent({
129
+ this.telemetry.recordMetric({
129
130
  eventName: 'decryptionError',
130
131
  volumeType: shareTypeToMetricContext(share.type),
131
132
  field: 'shareKey',
132
133
  fromBefore2024,
133
134
  error,
135
+ uid: share.shareId,
134
136
  });
135
137
  this.reportedDecryptionErrors.add(share.shareId);
136
138
  }
@@ -143,11 +145,12 @@ export class SharesCryptoService {
143
145
  const fromBefore2024 = share.creationTime ? share.creationTime < new Date('2024-01-01') : undefined;
144
146
  this.logger.error(`Failed to verify share ${share.shareId} (from before 2024: ${fromBefore2024})`);
145
147
 
146
- this.telemetry.logEvent({
148
+ this.telemetry.recordMetric({
147
149
  eventName: 'verificationError',
148
150
  volumeType: shareTypeToMetricContext(share.type),
149
151
  field: 'shareKey',
150
152
  fromBefore2024,
153
+ uid: share.shareId,
151
154
  });
152
155
  this.reportedVerificationErrors.add(share.shareId);
153
156
  }
@@ -4,7 +4,7 @@ import {
4
4
  DriveAPIService,
5
5
  drivePaths,
6
6
  nodeTypeNumberToNodeType,
7
- permissionsToDirectMemberRole,
7
+ permissionsToMemberRole,
8
8
  memberRoleToPermission,
9
9
  } from '../apiService';
10
10
  import {
@@ -224,7 +224,7 @@ export class SharingAPIService {
224
224
  base64KeyPacket: response.Invitation.KeyPacket,
225
225
  base64KeyPacketSignature: response.Invitation.KeyPacketSignature,
226
226
  invitationTime: new Date(response.Invitation.CreateTime * 1000),
227
- role: permissionsToDirectMemberRole(this.logger, response.Invitation.Permissions),
227
+ role: permissionsToMemberRole(this.logger, response.Invitation.Permissions),
228
228
  share: {
229
229
  armoredKey: response.Share.ShareKey,
230
230
  armoredPassphrase: response.Share.Passphrase,
@@ -312,7 +312,7 @@ export class SharingAPIService {
312
312
  base64KeyPacket: member.KeyPacket,
313
313
  base64KeyPacketSignature: member.KeyPacketSignature,
314
314
  invitationTime: new Date(member.CreateTime * 1000),
315
- role: permissionsToDirectMemberRole(this.logger, member.Permissions),
315
+ role: permissionsToMemberRole(this.logger, member.Permissions),
316
316
  };
317
317
  });
318
318
  }
@@ -469,7 +469,7 @@ export class SharingAPIService {
469
469
  uid: makePublicLinkUid(shareUrl.ShareID, shareUrl.ShareURLID),
470
470
  creationTime: new Date(shareUrl.CreateTime * 1000),
471
471
  expirationTime: shareUrl.ExpirationTime ? new Date(shareUrl.ExpirationTime * 1000) : undefined,
472
- role: permissionsToDirectMemberRole(this.logger, shareUrl.Permissions),
472
+ role: permissionsToMemberRole(this.logger, shareUrl.Permissions),
473
473
  flags: shareUrl.Flags,
474
474
  creatorEmail: shareUrl.CreatorEmail,
475
475
  publicUrl: shareUrl.PublicUrl,
@@ -588,7 +588,7 @@ export class SharingAPIService {
588
588
  addedByEmail: invitation.InviterEmail,
589
589
  inviteeEmail: invitation.InviteeEmail,
590
590
  invitationTime: new Date(invitation.CreateTime * 1000),
591
- role: permissionsToDirectMemberRole(this.logger, invitation.Permissions),
591
+ role: permissionsToMemberRole(this.logger, invitation.Permissions),
592
592
  base64KeyPacket: invitation.KeyPacket,
593
593
  base64KeyPacketSignature: invitation.KeyPacketSignature,
594
594
  };
@@ -605,7 +605,7 @@ export class SharingAPIService {
605
605
  addedByEmail: invitation.InviterEmail,
606
606
  inviteeEmail: invitation.InviteeEmail,
607
607
  invitationTime: new Date(invitation.CreateTime * 1000),
608
- role: permissionsToDirectMemberRole(this.logger, invitation.Permissions),
608
+ role: permissionsToMemberRole(this.logger, invitation.Permissions),
609
609
  base64Signature: invitation.ExternalInvitationSignature,
610
610
  state,
611
611
  };
@@ -84,7 +84,7 @@ describe('SharingCryptoService', () => {
84
84
  'armoredPassphrase',
85
85
  );
86
86
  expect(driveCrypto.decryptNodeName).toHaveBeenCalledWith('encryptedName', 'decryptedKey', []);
87
- expect(telemetry.logEvent).not.toHaveBeenCalled();
87
+ expect(telemetry.recordMetric).not.toHaveBeenCalled();
88
88
  });
89
89
 
90
90
  it('should handle undecryptable URL password', async () => {
@@ -97,11 +97,12 @@ describe('SharingCryptoService', () => {
97
97
  url: resultError(new Error('Failed to decrypt bookmark password: Failed to decrypt URL password')),
98
98
  nodeName: resultError(new Error('Failed to decrypt bookmark password: Failed to decrypt URL password')),
99
99
  });
100
- expect(telemetry.logEvent).toHaveBeenCalledWith({
100
+ expect(telemetry.recordMetric).toHaveBeenCalledWith({
101
101
  eventName: 'decryptionError',
102
102
  volumeType: MetricVolumeType.SharedPublic,
103
103
  field: 'shareUrlPassword',
104
104
  error,
105
+ uid: 'tokenId',
105
106
  });
106
107
  });
107
108
 
@@ -115,11 +116,12 @@ describe('SharingCryptoService', () => {
115
116
  url: resultOk('https://drive.proton.me/urls/tokenId#urlPassword'),
116
117
  nodeName: resultError(new Error('Failed to decrypt bookmark key: Failed to decrypt share key')),
117
118
  });
118
- expect(telemetry.logEvent).toHaveBeenCalledWith({
119
+ expect(telemetry.recordMetric).toHaveBeenCalledWith({
119
120
  eventName: 'decryptionError',
120
121
  volumeType: MetricVolumeType.SharedPublic,
121
122
  field: 'shareKey',
122
123
  error,
124
+ uid: 'tokenId',
123
125
  });
124
126
  });
125
127
 
@@ -133,11 +135,12 @@ describe('SharingCryptoService', () => {
133
135
  url: resultOk('https://drive.proton.me/urls/tokenId#urlPassword'),
134
136
  nodeName: resultError(new Error('Failed to decrypt bookmark name: Failed to decrypt node name')),
135
137
  });
136
- expect(telemetry.logEvent).toHaveBeenCalledWith({
138
+ expect(telemetry.recordMetric).toHaveBeenCalledWith({
137
139
  eventName: 'decryptionError',
138
140
  volumeType: MetricVolumeType.SharedPublic,
139
141
  field: 'nodeName',
140
142
  error,
143
+ uid: 'tokenId',
141
144
  });
142
145
  });
143
146
 
@@ -148,7 +148,7 @@ export class SharingCryptoService {
148
148
  }
149
149
  const addressPublicKeys = await this.account.getPublicKeys(share.creatorEmail);
150
150
 
151
- const { key, passphraseSessionKey, verified } = await this.driveCrypto.decryptKey(
151
+ const { key, passphraseSessionKey, verified, verificationErrors } = await this.driveCrypto.decryptKey(
152
152
  share.encryptedCrypto.armoredKey,
153
153
  share.encryptedCrypto.armoredPassphrase,
154
154
  share.encryptedCrypto.armoredPassphraseSignature,
@@ -161,7 +161,7 @@ export class SharingCryptoService {
161
161
  ? resultOk(share.creatorEmail)
162
162
  : resultError({
163
163
  claimedAuthor: share.creatorEmail,
164
- error: getVerificationMessage(verified),
164
+ error: getVerificationMessage(verified, verificationErrors),
165
165
  });
166
166
 
167
167
  return {
@@ -478,11 +478,12 @@ export class SharingCryptoService {
478
478
 
479
479
  return urlPassword;
480
480
  } catch (error: unknown) {
481
- this.telemetry.logEvent({
481
+ this.telemetry.recordMetric({
482
482
  eventName: 'decryptionError',
483
483
  volumeType: MetricVolumeType.SharedPublic,
484
484
  field: 'shareUrlPassword',
485
485
  error,
486
+ uid: encryptedBookmark.tokenId,
486
487
  });
487
488
 
488
489
  const message = getErrorMessage(error);
@@ -503,11 +504,12 @@ export class SharingCryptoService {
503
504
 
504
505
  return shareKey;
505
506
  } catch (error: unknown) {
506
- this.telemetry.logEvent({
507
+ this.telemetry.recordMetric({
507
508
  eventName: 'decryptionError',
508
509
  volumeType: MetricVolumeType.SharedPublic,
509
510
  field: 'shareKey',
510
511
  error,
512
+ uid: encryptedBookmark.tokenId,
511
513
  });
512
514
 
513
515
  const message = getErrorMessage(error);
@@ -535,11 +537,12 @@ export class SharingCryptoService {
535
537
 
536
538
  return resultOk(name);
537
539
  } catch (error: unknown) {
538
- this.telemetry.logEvent({
540
+ this.telemetry.recordMetric({
539
541
  eventName: 'decryptionError',
540
542
  volumeType: MetricVolumeType.SharedPublic,
541
543
  field: 'nodeName',
542
544
  error,
545
+ uid: encryptedBookmark.tokenId,
543
546
  });
544
547
 
545
548
  const message = getErrorMessage(error);