@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
@@ -0,0 +1,182 @@
1
+ interface UnderlyingSeekableSource extends UnderlyingDefaultSource<Uint8Array> {
2
+ seek: (position: number) => void | Promise<void>;
3
+ }
4
+
5
+ /**
6
+ * A seekable readable stream that can be used to seek to a specific position
7
+ * in the stream.
8
+ *
9
+ * This is useful for downloading the file in chunks or jumping to a specific
10
+ * position in the file when streaming a video.
11
+ *
12
+ * Example to get next chunk of data from the stream at position 100:
13
+ *
14
+ * ```
15
+ * const stream = new SeekableReadableStream(underlyingSource);
16
+ * const reader = stream.getReader();
17
+ * await stream.seek(100);
18
+ * const data = await stream.read();
19
+ * console.log(data);
20
+ * ```
21
+ */
22
+ export class SeekableReadableStream extends ReadableStream<Uint8Array> {
23
+ private seekCallback: (position: number) => void | Promise<void>;
24
+
25
+ constructor({ seek, ...underlyingSource }: UnderlyingSeekableSource, queuingStrategy?: QueuingStrategy<Uint8Array>) {
26
+ super(underlyingSource, queuingStrategy);
27
+ this.seekCallback = seek;
28
+ }
29
+
30
+ seek(position: number): void | Promise<void> {
31
+ return this.seekCallback(position);
32
+ }
33
+ }
34
+
35
+ /**
36
+ * A buffered seekable stream that allows to seek and read specific number of
37
+ * bytes from the stream.
38
+ *
39
+ * This is useful for reading specific range of data from the stream. Example
40
+ * being video player buffering the next several bytes.
41
+ *
42
+ * The underlying source can chunk the data into various sizes. To ensure that
43
+ * every read operation is for the correct location, the SeekableStream is not
44
+ * queueing the data upfront. Instead, it will read the data and buffer it for
45
+ * the next read operation. If seek is called, the internal buffer is updated
46
+ * accordingly.
47
+ *
48
+ * Example to read 10 bytes from the stream at position 100:
49
+ *
50
+ * ```
51
+ * const stream = new BufferedSeekableStream(underlyingSource);
52
+ * await stream.seek(100);
53
+ * const data = await stream.read(10);
54
+ * console.log(data);
55
+ * ```
56
+ */
57
+ export class BufferedSeekableStream extends SeekableReadableStream {
58
+ private buffer: Uint8Array = new Uint8Array(0);
59
+ private bufferPosition: number = 0;
60
+ private reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
61
+ private streamClosed: boolean = false;
62
+ private currentPosition: number = 0;
63
+
64
+ constructor(underlyingSource: UnderlyingSeekableSource, queuingStrategy?: QueuingStrategy<Uint8Array>) {
65
+ // highWaterMark means that the stream will buffer up to this many
66
+ // bytes. We do not want to buffer anything
67
+ if (queuingStrategy && queuingStrategy.highWaterMark !== 0) {
68
+ throw new Error('highWaterMark must be 0');
69
+ }
70
+
71
+ super(underlyingSource, {
72
+ ...queuingStrategy,
73
+ highWaterMark: 0,
74
+ });
75
+
76
+ this.reader = super.getReader();
77
+ }
78
+
79
+ /**
80
+ * Read a specific number of bytes from the stream.
81
+ *
82
+ * When the underlying source provides more bytes than requested, the
83
+ * remaining bytes are buffered and used for the next read operation.
84
+ *
85
+ * @param numBytes - Number of bytes to read
86
+ * @returns Promise<Uint8Array> The read bytes
87
+ */
88
+ async read(numBytes: number): Promise<{ value: Uint8Array; done: boolean }> {
89
+ if (numBytes <= 0) {
90
+ throw new Error('Invalid number of bytes to read');
91
+ }
92
+
93
+ await this.ensureBufferSize(numBytes);
94
+
95
+ const result = this.buffer.slice(this.bufferPosition, this.bufferPosition + numBytes);
96
+ this.bufferPosition += numBytes;
97
+ this.currentPosition += numBytes;
98
+ return {
99
+ value: result,
100
+ done: this.streamClosed,
101
+ };
102
+ }
103
+
104
+ private async ensureBufferSize(minBytes: number): Promise<void> {
105
+ const availableBytes = this.buffer.length - this.bufferPosition;
106
+ const neededBytes = minBytes - availableBytes;
107
+
108
+ if (neededBytes <= 0 || this.streamClosed) {
109
+ return;
110
+ }
111
+
112
+ const chunks: Uint8Array[] = [];
113
+ let totalBytesRead = 0;
114
+
115
+ while (totalBytesRead < neededBytes && !this.streamClosed) {
116
+ if (!this.reader) {
117
+ throw new Error('Stream reader is not available');
118
+ }
119
+
120
+ const { done, value } = await this.reader.read();
121
+
122
+ if (done) {
123
+ this.streamClosed = true;
124
+ break;
125
+ }
126
+
127
+ if (value) {
128
+ chunks.push(value);
129
+ totalBytesRead += value.length;
130
+ }
131
+ }
132
+
133
+ if (chunks.length > 0) {
134
+ // Create new buffer with existing unused data plus new chunks
135
+ const unusedBufferData = this.buffer.slice(this.bufferPosition);
136
+ const newTotalLength = unusedBufferData.length + totalBytesRead;
137
+ const newBuffer = new Uint8Array(newTotalLength);
138
+
139
+ newBuffer.set(unusedBufferData, 0);
140
+ let offset = unusedBufferData.length;
141
+ for (const chunk of chunks) {
142
+ newBuffer.set(chunk, offset);
143
+ offset += chunk.length;
144
+ }
145
+
146
+ this.buffer = newBuffer;
147
+ this.bufferPosition = 0;
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Seek to the given position in the stream.
153
+ *
154
+ * If the position is outside of internally buffered data, the buffer is
155
+ * cleared. If the position is seeked back, the buffer is read again from
156
+ * the underlying source.
157
+ *
158
+ * @param position - The position to seek to in bytes.
159
+ */
160
+ async seek(position: number): Promise<void> {
161
+ const endOfBufferPosition = this.currentPosition + (this.buffer.length - this.bufferPosition);
162
+
163
+ if (position > endOfBufferPosition) {
164
+ this.buffer = new Uint8Array(0);
165
+ this.bufferPosition = 0;
166
+ } else if (position < this.currentPosition) {
167
+ this.buffer = new Uint8Array(0);
168
+ this.bufferPosition = 0;
169
+ } else {
170
+ this.bufferPosition += position - this.currentPosition;
171
+ }
172
+
173
+ await super.seek(position);
174
+
175
+ if (this.reader) {
176
+ this.reader.releaseLock();
177
+ }
178
+ this.reader = super.getReader();
179
+ this.streamClosed = false;
180
+ this.currentPosition = position;
181
+ }
182
+ }
@@ -14,7 +14,7 @@ describe('DownloadTelemetry', () => {
14
14
 
15
15
  beforeEach(() => {
16
16
  mockTelemetry = {
17
- logEvent: jest.fn(),
17
+ recordMetric: jest.fn(),
18
18
  getLogger: jest.fn().mockReturnValue({
19
19
  info: jest.fn(),
20
20
  warn: jest.fn(),
@@ -34,7 +34,7 @@ describe('DownloadTelemetry', () => {
34
34
  const error = new Error('Failed');
35
35
  await downloadTelemetry.downloadInitFailed(nodeUid, error);
36
36
 
37
- expect(mockTelemetry.logEvent).toHaveBeenCalledWith({
37
+ expect(mockTelemetry.recordMetric).toHaveBeenCalledWith({
38
38
  eventName: 'download',
39
39
  volumeType: 'own_volume',
40
40
  downloadedSize: 0,
@@ -47,7 +47,7 @@ describe('DownloadTelemetry', () => {
47
47
  const error = new Error('Failed');
48
48
  await downloadTelemetry.downloadFailed(revisionUid, error, 123, 456);
49
49
 
50
- expect(mockTelemetry.logEvent).toHaveBeenCalledWith({
50
+ expect(mockTelemetry.recordMetric).toHaveBeenCalledWith({
51
51
  eventName: 'download',
52
52
  volumeType: 'own_volume',
53
53
  downloadedSize: 123,
@@ -60,7 +60,7 @@ describe('DownloadTelemetry', () => {
60
60
  it('should log successful download (excludes error)', async () => {
61
61
  await downloadTelemetry.downloadFinished(revisionUid, 500);
62
62
 
63
- expect(mockTelemetry.logEvent).toHaveBeenCalledWith({
63
+ expect(mockTelemetry.recordMetric).toHaveBeenCalledWith({
64
64
  eventName: 'download',
65
65
  volumeType: 'own_volume',
66
66
  downloadedSize: 500,
@@ -70,7 +70,7 @@ describe('DownloadTelemetry', () => {
70
70
 
71
71
  describe('detect error category', () => {
72
72
  const verifyErrorCategory = (error: string) => {
73
- expect(mockTelemetry.logEvent).toHaveBeenCalledWith(
73
+ expect(mockTelemetry.recordMetric).toHaveBeenCalledWith(
74
74
  expect.objectContaining({
75
75
  error,
76
76
  }),
@@ -80,7 +80,7 @@ describe('DownloadTelemetry', () => {
80
80
  it('should ignore ValidationError', async () => {
81
81
  const error = new ValidationError('Validation error');
82
82
  await downloadTelemetry.downloadFailed(revisionUid, error, 100, 200);
83
- expect(mockTelemetry.logEvent).not.toHaveBeenCalled();
83
+ expect(mockTelemetry.recordMetric).not.toHaveBeenCalled();
84
84
  });
85
85
 
86
86
  it('should ignore AbortError', async () => {
@@ -88,7 +88,7 @@ describe('DownloadTelemetry', () => {
88
88
  error.name = 'AbortError';
89
89
  await downloadTelemetry.downloadFailed(revisionUid, error, 100, 200);
90
90
 
91
- expect(mockTelemetry.logEvent).not.toHaveBeenCalled();
91
+ expect(mockTelemetry.recordMetric).not.toHaveBeenCalled();
92
92
  });
93
93
 
94
94
  it('should detect "rate_limited" error for RateLimitedError', async () => {
@@ -80,7 +80,7 @@ export class DownloadTelemetry {
80
80
  this.logger.error('Failed to get metric volume type', error);
81
81
  }
82
82
 
83
- this.telemetry.logEvent({
83
+ this.telemetry.recordMetric({
84
84
  eventName: 'download',
85
85
  volumeType,
86
86
  ...options,
@@ -2,20 +2,54 @@ import { VERIFICATION_STATUS } from '../crypto';
2
2
  import { getVerificationMessage } from './errors';
3
3
 
4
4
  describe('getVerificationMessage', () => {
5
- const testCases: [VERIFICATION_STATUS, string | undefined, boolean, string][] = [
6
- [VERIFICATION_STATUS.NOT_SIGNED, 'type', false, 'Missing signature for type'],
7
- [VERIFICATION_STATUS.NOT_SIGNED, undefined, false, 'Missing signature'],
8
- [VERIFICATION_STATUS.NOT_SIGNED, 'type', true, 'Missing signature for type'],
9
- [VERIFICATION_STATUS.NOT_SIGNED, undefined, true, 'Missing signature'],
10
- [VERIFICATION_STATUS.SIGNED_AND_INVALID, 'type', false, 'Signature verification for type failed'],
11
- [VERIFICATION_STATUS.SIGNED_AND_INVALID, undefined, false, 'Signature verification failed'],
12
- [VERIFICATION_STATUS.SIGNED_AND_INVALID, 'type', true, 'Verification keys for type are not available'],
13
- [VERIFICATION_STATUS.SIGNED_AND_INVALID, undefined, true, 'Verification keys are not available'],
5
+ const testCases: [VERIFICATION_STATUS, Error[] | undefined, string | undefined, boolean, string][] = [
6
+ [VERIFICATION_STATUS.NOT_SIGNED, undefined, 'type', false, 'Missing signature for type'],
7
+ [VERIFICATION_STATUS.NOT_SIGNED, undefined, undefined, false, 'Missing signature'],
8
+ [VERIFICATION_STATUS.NOT_SIGNED, undefined, 'type', true, 'Missing signature for type'],
9
+ [VERIFICATION_STATUS.NOT_SIGNED, undefined, undefined, true, 'Missing signature'],
10
+ [VERIFICATION_STATUS.SIGNED_AND_INVALID, undefined, 'type', false, 'Signature verification for type failed'],
11
+ [VERIFICATION_STATUS.SIGNED_AND_INVALID, undefined, undefined, false, 'Signature verification failed'],
12
+ [
13
+ VERIFICATION_STATUS.SIGNED_AND_INVALID,
14
+ undefined,
15
+ 'type',
16
+ true,
17
+ 'Verification keys for type are not available',
18
+ ],
19
+ [VERIFICATION_STATUS.SIGNED_AND_INVALID, undefined, undefined, true, 'Verification keys are not available'],
20
+ [
21
+ VERIFICATION_STATUS.SIGNED_AND_INVALID,
22
+ [new Error('error1'), new Error('error2')],
23
+ undefined,
24
+ false,
25
+ 'Signature verification failed: error1, error2',
26
+ ],
27
+ [
28
+ VERIFICATION_STATUS.SIGNED_AND_INVALID,
29
+ [new Error('error1'), new Error('error2')],
30
+ 'type',
31
+ false,
32
+ 'Signature verification for type failed: error1, error2',
33
+ ],
34
+ [
35
+ VERIFICATION_STATUS.SIGNED_AND_INVALID,
36
+ [new Error('error1'), new Error('error2')],
37
+ undefined,
38
+ true,
39
+ 'Verification keys are not available',
40
+ ],
41
+ [
42
+ VERIFICATION_STATUS.SIGNED_AND_INVALID,
43
+ [new Error('error1'), new Error('error2')],
44
+ 'type',
45
+ true,
46
+ 'Verification keys for type are not available',
47
+ ],
14
48
  ];
15
49
 
16
- for (const [status, type, notAvailable, expected] of testCases) {
50
+ for (const [status, errors, type, notAvailable, expected] of testCases) {
17
51
  it(`returns correct message for status ${status} with type ${type} and notAvailable ${notAvailable}`, () => {
18
- expect(getVerificationMessage(status, type, notAvailable)).toBe(expected);
52
+ expect(getVerificationMessage(status, errors, type, notAvailable)).toBe(expected);
19
53
  });
20
54
  }
21
55
  });
@@ -11,6 +11,7 @@ export function getErrorMessage(error: unknown): string {
11
11
  */
12
12
  export function getVerificationMessage(
13
13
  verified: VERIFICATION_STATUS,
14
+ verificationErrors?: Error[],
14
15
  signatureType?: string,
15
16
  notAvailableVerificationKeys = false,
16
17
  ): string {
@@ -24,6 +25,13 @@ export function getVerificationMessage(
24
25
  : c('Error').t`Verification keys are not available`;
25
26
  }
26
27
 
28
+ if (verificationErrors) {
29
+ const errorMessage = verificationErrors?.map((e) => e.message).join(', ');
30
+ return signatureType
31
+ ? c('Error').t`Signature verification for ${signatureType} failed: ${errorMessage}`
32
+ : c('Error').t`Signature verification failed: ${errorMessage}`;
33
+ }
34
+
27
35
  return signatureType
28
36
  ? c('Error').t`Signature verification for ${signatureType} failed`
29
37
  : c('Error').t`Signature verification failed`;
@@ -121,7 +121,7 @@ export class DriveEventsService {
121
121
  }
122
122
 
123
123
  private sendNumberOfVolumeSubscriptionsToTelemetry() {
124
- this.telemetry.logEvent({
124
+ this.telemetry.recordMetric({
125
125
  eventName: 'volumeEventsSubscriptionsChanged',
126
126
  numberOfVolumeSubscriptions: Object.keys(this.volumeEventManagers).length,
127
127
  });
@@ -76,7 +76,7 @@ function generateAPINode() {
76
76
  };
77
77
  }
78
78
 
79
- function generateFileNode(overrides = {}) {
79
+ function generateFileNode(overrides = {}, encryptedCryptoOverrides = {}) {
80
80
  const node = generateNode();
81
81
  return {
82
82
  ...node,
@@ -98,12 +98,13 @@ function generateFileNode(overrides = {}) {
98
98
  armoredExtendedAttributes: '{file}',
99
99
  thumbnails: [],
100
100
  },
101
+ ...encryptedCryptoOverrides,
101
102
  },
102
103
  ...overrides,
103
104
  };
104
105
  }
105
106
 
106
- function generateFolderNode(overrides = {}) {
107
+ function generateFolderNode(overrides = {}, encryptedCryptoOverrides = {}) {
107
108
  const node = generateNode();
108
109
  return {
109
110
  ...node,
@@ -114,6 +115,7 @@ function generateFolderNode(overrides = {}) {
114
115
  armoredHashKey: 'nodeHashKey',
115
116
  armoredExtendedAttributes: '{folder}',
116
117
  },
118
+ ...encryptedCryptoOverrides,
117
119
  },
118
120
  ...overrides,
119
121
  };
@@ -140,7 +142,8 @@ function generateNode() {
140
142
 
141
143
  shareId: undefined,
142
144
  isShared: false,
143
- directMemberRole: MemberRole.Admin,
145
+ directRole: MemberRole.Admin,
146
+ membership: undefined,
144
147
 
145
148
  encryptedCrypto: {
146
149
  armoredKey: 'nodeKey',
@@ -148,6 +151,7 @@ function generateNode() {
148
151
  armoredNodePassphraseSignature: 'nodePassSig',
149
152
  nameSignatureEmail: 'nameSigEmail',
150
153
  signatureEmail: 'sigEmail',
154
+ membership: undefined,
151
155
  },
152
156
  };
153
157
  }
@@ -211,14 +215,34 @@ describe('nodeAPIService', () => {
211
215
  },
212
216
  Membership: {
213
217
  Permissions: 22,
218
+ InviteTime: 1234567890,
219
+ InviterEmail: 'inviterEmail',
220
+ MemberSharePassphraseKeyPacket: 'memberSharePassphraseKeyPacket',
221
+ InviterSharePassphraseKeyPacketSignature: 'inviterSharePassphraseKeyPacketSignature',
222
+ InviteeSharePassphraseSessionKeySignature: 'inviteeSharePassphraseSessionKeySignature',
223
+ },
224
+ },
225
+ ),
226
+ generateFolderNode(
227
+ {
228
+ isShared: true,
229
+ shareId: 'shareId',
230
+ directRole: MemberRole.Admin,
231
+ membership: {
232
+ role: MemberRole.Admin,
233
+ inviteTime: new Date(1234567890000),
234
+ },
235
+ },
236
+ {
237
+ membership: {
238
+ inviterEmail: 'inviterEmail',
239
+ base64MemberSharePassphraseKeyPacket: 'memberSharePassphraseKeyPacket',
240
+ armoredInviterSharePassphraseKeyPacketSignature: 'inviterSharePassphraseKeyPacketSignature',
241
+ armoredInviteeSharePassphraseSessionKeySignature:
242
+ 'inviteeSharePassphraseSessionKeySignature',
214
243
  },
215
244
  },
216
245
  ),
217
- generateFolderNode({
218
- isShared: true,
219
- shareId: 'shareId',
220
- directMemberRole: MemberRole.Admin,
221
- }),
222
246
  );
223
247
  });
224
248
 
@@ -232,14 +256,34 @@ describe('nodeAPIService', () => {
232
256
  },
233
257
  Membership: {
234
258
  Permissions: 42,
259
+ InviteTime: 1234567890,
260
+ InviterEmail: 'inviterEmail',
261
+ MemberSharePassphraseKeyPacket: 'memberSharePassphraseKeyPacket',
262
+ InviterSharePassphraseKeyPacketSignature: 'inviterSharePassphraseKeyPacketSignature',
263
+ InviteeSharePassphraseSessionKeySignature: 'inviteeSharePassphraseSessionKeySignature',
264
+ },
265
+ },
266
+ ),
267
+ generateFolderNode(
268
+ {
269
+ isShared: true,
270
+ shareId: 'shareId',
271
+ directRole: MemberRole.Viewer,
272
+ membership: {
273
+ role: MemberRole.Viewer,
274
+ inviteTime: new Date(1234567890000),
275
+ },
276
+ },
277
+ {
278
+ membership: {
279
+ inviterEmail: 'inviterEmail',
280
+ base64MemberSharePassphraseKeyPacket: 'memberSharePassphraseKeyPacket',
281
+ armoredInviterSharePassphraseKeyPacketSignature: 'inviterSharePassphraseKeyPacketSignature',
282
+ armoredInviteeSharePassphraseSessionKeySignature:
283
+ 'inviteeSharePassphraseSessionKeySignature',
235
284
  },
236
285
  },
237
286
  ),
238
- generateFolderNode({
239
- isShared: true,
240
- shareId: 'shareId',
241
- directMemberRole: MemberRole.Viewer,
242
- }),
243
287
  'myVolumeId',
244
288
  );
245
289
  });
@@ -308,12 +352,12 @@ describe('nodeAPIService', () => {
308
352
  generateFolderNode({
309
353
  uid: 'volumeId1~nodeId1',
310
354
  parentUid: 'volumeId1~parentNodeId1',
311
- directMemberRole: MemberRole.Admin,
355
+ directRole: MemberRole.Admin,
312
356
  }),
313
357
  generateFolderNode({
314
358
  uid: 'volumeId2~nodeId2',
315
359
  parentUid: 'volumeId2~parentNodeId2',
316
- directMemberRole: MemberRole.Inherited,
360
+ directRole: MemberRole.Inherited,
317
361
  }),
318
362
  ]);
319
363
  });
@@ -8,7 +8,7 @@ import {
8
8
  drivePaths,
9
9
  isCodeOk,
10
10
  nodeTypeNumberToNodeType,
11
- permissionsToDirectMemberRole,
11
+ permissionsToMemberRole,
12
12
  } from '../apiService';
13
13
  import { asyncIteratorRace } from '../asyncIteratorRace';
14
14
  import { batch } from '../batch';
@@ -471,6 +471,8 @@ function linkToEncryptedNode(
471
471
  link: PostLoadLinksMetadataResponse['Links'][0],
472
472
  isAdmin: boolean,
473
473
  ): EncryptedNode {
474
+ const membershipRole = permissionsToMemberRole(logger, link.Membership?.Permissions);
475
+
474
476
  const baseNodeMetadata = {
475
477
  // Internal metadata
476
478
  hash: link.Link.NameHash || undefined,
@@ -486,16 +488,31 @@ function linkToEncryptedNode(
486
488
  // Sharing node metadata
487
489
  shareId: link.Sharing?.ShareID || undefined,
488
490
  isShared: !!link.Sharing,
489
- directMemberRole: isAdmin
490
- ? MemberRole.Admin
491
- : permissionsToDirectMemberRole(logger, link.Membership?.Permissions),
491
+ directRole: isAdmin ? MemberRole.Admin : membershipRole,
492
+ membership: link.Membership
493
+ ? {
494
+ role: membershipRole,
495
+ inviteTime: new Date(link.Membership.InviteTime * 1000),
496
+ }
497
+ : undefined,
492
498
  };
499
+
493
500
  const baseCryptoNodeMetadata = {
494
501
  signatureEmail: link.Link.SignatureEmail || undefined,
495
502
  nameSignatureEmail: link.Link.NameSignatureEmail || undefined,
496
503
  armoredKey: link.Link.NodeKey,
497
504
  armoredNodePassphrase: link.Link.NodePassphrase,
498
505
  armoredNodePassphraseSignature: link.Link.NodePassphraseSignature,
506
+ membership: link.Membership
507
+ ? {
508
+ inviterEmail: link.Membership.InviterEmail,
509
+ base64MemberSharePassphraseKeyPacket: link.Membership.MemberSharePassphraseKeyPacket,
510
+ armoredInviterSharePassphraseKeyPacketSignature:
511
+ link.Membership.InviterSharePassphraseKeyPacketSignature,
512
+ armoredInviteeSharePassphraseSessionKeySignature:
513
+ link.Membership.InviteeSharePassphraseSessionKeySignature,
514
+ }
515
+ : undefined,
499
516
  };
500
517
 
501
518
  if (link.Link.Type === 1 && link.Folder) {
@@ -12,7 +12,12 @@ function generateNode(
12
12
  return {
13
13
  uid: `${params.volumeId || 'volumeId'}~:${uid}`,
14
14
  parentUid: `${params.volumeId || 'volumeId'}~:${parentUid}`,
15
- directMemberRole: MemberRole.Admin,
15
+ directRole: MemberRole.Admin,
16
+ membership: {
17
+ role: MemberRole.Admin,
18
+ inviteTime: new Date(),
19
+ sharedBy: resultOk('test@example.com'),
20
+ },
16
21
  type: NodeType.File,
17
22
  mediaType: 'text',
18
23
  isShared: false,
@@ -252,8 +252,9 @@ function deserialiseNode(nodeData: string): DecryptedNode {
252
252
  typeof node !== 'object' ||
253
253
  !node.uid ||
254
254
  typeof node.uid !== 'string' ||
255
- !node.directMemberRole ||
256
- typeof node.directMemberRole !== 'string' ||
255
+ !node.directRole ||
256
+ typeof node.directRole !== 'string' ||
257
+ (typeof node.membership !== 'object' && node.membership !== undefined) ||
257
258
  !node.type ||
258
259
  typeof node.type !== 'string' ||
259
260
  (typeof node.mediaType !== 'string' && node.mediaType !== undefined) ||
@@ -271,6 +272,12 @@ function deserialiseNode(nodeData: string): DecryptedNode {
271
272
  creationTime: new Date(node.creationTime),
272
273
  trashTime: node.trashTime ? new Date(node.trashTime) : undefined,
273
274
  activeRevision: node.activeRevision ? deserialiseRevision(node.activeRevision) : undefined,
275
+ membership: node.membership
276
+ ? {
277
+ ...node.membership,
278
+ inviteTime: new Date(node.membership.inviteTime),
279
+ }
280
+ : undefined,
274
281
  folder: node.folder
275
282
  ? {
276
283
  ...node.folder,