@towns-protocol/sdk 0.0.252 → 0.0.255

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 (176) hide show
  1. package/dist/client.d.ts +2 -1
  2. package/dist/client.d.ts.map +1 -1
  3. package/dist/client.js +6 -5
  4. package/dist/client.js.map +1 -1
  5. package/dist/clientDecryptionExtensions.d.ts +2 -1
  6. package/dist/clientDecryptionExtensions.d.ts.map +1 -1
  7. package/dist/clientDecryptionExtensions.js +1 -1
  8. package/dist/clientDecryptionExtensions.js.map +1 -1
  9. package/dist/decryptionExtensions.d.ts +199 -0
  10. package/dist/decryptionExtensions.d.ts.map +1 -0
  11. package/dist/decryptionExtensions.js +678 -0
  12. package/dist/decryptionExtensions.js.map +1 -0
  13. package/dist/index.d.ts +21 -5
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +21 -5
  16. package/dist/index.js.map +1 -1
  17. package/dist/observable/combine.d.ts +4 -2
  18. package/dist/observable/combine.d.ts.map +1 -1
  19. package/dist/observable/combine.js +5 -2
  20. package/dist/observable/combine.js.map +1 -1
  21. package/dist/observable/observable.d.ts +1 -1
  22. package/dist/observable/observable.d.ts.map +1 -1
  23. package/dist/observable/observable.js +1 -4
  24. package/dist/observable/observable.js.map +1 -1
  25. package/dist/observable/observableRecord.d.ts +18 -0
  26. package/dist/observable/observableRecord.d.ts.map +1 -0
  27. package/dist/observable/observableRecord.js +71 -0
  28. package/dist/observable/observableRecord.js.map +1 -0
  29. package/dist/observable/utils.d.ts +2 -0
  30. package/dist/observable/utils.d.ts.map +1 -0
  31. package/dist/observable/utils.js +12 -0
  32. package/dist/observable/utils.js.map +1 -0
  33. package/dist/stream.d.ts +1 -1
  34. package/dist/stream.d.ts.map +1 -1
  35. package/dist/streamEvents.d.ts +2 -1
  36. package/dist/streamEvents.d.ts.map +1 -1
  37. package/dist/streamStateView.d.ts +1 -1
  38. package/dist/streamStateView.d.ts.map +1 -1
  39. package/dist/streamStateView.js +9 -9
  40. package/dist/streamStateView.js.map +1 -1
  41. package/dist/streamStateView_ChannelMetadata.d.ts +10 -10
  42. package/dist/streamStateView_ChannelMetadata.d.ts.map +1 -1
  43. package/dist/streamStateView_ChannelMetadata.js +43 -23
  44. package/dist/streamStateView_ChannelMetadata.js.map +1 -1
  45. package/dist/streamStateView_DMChannel.d.ts +7 -4
  46. package/dist/streamStateView_DMChannel.d.ts.map +1 -1
  47. package/dist/streamStateView_DMChannel.js +18 -8
  48. package/dist/streamStateView_DMChannel.js.map +1 -1
  49. package/dist/streamStateView_GDMChannel.d.ts +5 -2
  50. package/dist/streamStateView_GDMChannel.d.ts.map +1 -1
  51. package/dist/streamStateView_GDMChannel.js +12 -5
  52. package/dist/streamStateView_GDMChannel.js.map +1 -1
  53. package/dist/streamStateView_Members.d.ts +5 -2
  54. package/dist/streamStateView_Members.d.ts.map +1 -1
  55. package/dist/streamStateView_Members.js +11 -1
  56. package/dist/streamStateView_Members.js.map +1 -1
  57. package/dist/streamStateView_Members_Solicitations.d.ts +1 -1
  58. package/dist/streamStateView_Members_Solicitations.d.ts.map +1 -1
  59. package/dist/streamStateView_Space.d.ts +5 -8
  60. package/dist/streamStateView_Space.d.ts.map +1 -1
  61. package/dist/streamStateView_Space.js +26 -23
  62. package/dist/streamStateView_Space.js.map +1 -1
  63. package/dist/streamStateView_User.d.ts +11 -18
  64. package/dist/streamStateView_User.d.ts.map +1 -1
  65. package/dist/streamStateView_User.js +35 -20
  66. package/dist/streamStateView_User.js.map +1 -1
  67. package/dist/streamStateView_UserInbox.d.ts +4 -1
  68. package/dist/streamStateView_UserInbox.d.ts.map +1 -1
  69. package/dist/streamStateView_UserInbox.js +6 -1
  70. package/dist/streamStateView_UserInbox.js.map +1 -1
  71. package/dist/streamStateView_UserMetadata.d.ts +4 -1
  72. package/dist/streamStateView_UserMetadata.d.ts.map +1 -1
  73. package/dist/streamStateView_UserMetadata.js +6 -1
  74. package/dist/streamStateView_UserMetadata.js.map +1 -1
  75. package/dist/streamStateView_UserSettings.d.ts +4 -3
  76. package/dist/streamStateView_UserSettings.d.ts.map +1 -1
  77. package/dist/streamStateView_UserSettings.js +11 -12
  78. package/dist/streamStateView_UserSettings.js.map +1 -1
  79. package/dist/sync-agent/entitlements/entitlements.d.ts +1 -1
  80. package/dist/sync-agent/river-connection/models/transactionalClient.d.ts +2 -1
  81. package/dist/sync-agent/river-connection/models/transactionalClient.d.ts.map +1 -1
  82. package/dist/sync-agent/river-connection/models/transactionalClient.js.map +1 -1
  83. package/dist/sync-agent/river-connection/riverConnection.d.ts +2 -1
  84. package/dist/sync-agent/river-connection/riverConnection.d.ts.map +1 -1
  85. package/dist/sync-agent/river-connection/riverConnection.js.map +1 -1
  86. package/dist/sync-agent/spaces/models/space.js +1 -1
  87. package/dist/sync-agent/spaces/models/space.js.map +1 -1
  88. package/dist/sync-agent/timeline/models/timeline-types.d.ts +1 -2
  89. package/dist/sync-agent/timeline/models/timeline-types.d.ts.map +1 -1
  90. package/dist/sync-agent/timeline/models/timeline-types.js +1 -2
  91. package/dist/sync-agent/timeline/models/timeline-types.js.map +1 -1
  92. package/dist/sync-agent/user/models/userMemberships.d.ts.map +1 -1
  93. package/dist/sync-agent/user/models/userMemberships.js +3 -1
  94. package/dist/sync-agent/user/models/userMemberships.js.map +1 -1
  95. package/dist/syncedStream.js +1 -1
  96. package/dist/syncedStream.js.map +1 -1
  97. package/dist/tests/multi/channelSpaceSettings.test.js +23 -23
  98. package/dist/tests/multi/channelSpaceSettings.test.js.map +1 -1
  99. package/dist/tests/multi_ne/client.test.js +2 -1
  100. package/dist/tests/multi_ne/client.test.js.map +1 -1
  101. package/dist/tests/multi_ne/space.test.js +5 -7
  102. package/dist/tests/multi_ne/space.test.js.map +1 -1
  103. package/dist/tests/multi_ne/streamStateView_User.test.js +3 -3
  104. package/dist/tests/multi_ne/streamStateView_User.test.js.map +1 -1
  105. package/dist/tests/testUtils.d.ts +1 -1
  106. package/dist/tests/testUtils.d.ts.map +1 -1
  107. package/dist/tests/unit/decryptionExtensions.test.d.ts +2 -0
  108. package/dist/tests/unit/decryptionExtensions.test.d.ts.map +1 -0
  109. package/dist/tests/unit/decryptionExtensions.test.js +353 -0
  110. package/dist/tests/unit/decryptionExtensions.test.js.map +1 -0
  111. package/dist/tests/unit/observable/combine.test.js +23 -23
  112. package/dist/tests/unit/observable/combine.test.js.map +1 -1
  113. package/dist/types.d.ts +1 -1
  114. package/dist/types.d.ts.map +1 -1
  115. package/dist/views/streams/channelStreams.d.ts +8 -0
  116. package/dist/views/streams/channelStreams.d.ts.map +1 -0
  117. package/dist/views/streams/channelStreams.js +9 -0
  118. package/dist/views/streams/channelStreams.js.map +1 -0
  119. package/dist/views/streams/dmStreams.d.ts +13 -0
  120. package/dist/views/streams/dmStreams.d.ts.map +1 -0
  121. package/dist/views/streams/dmStreams.js +33 -0
  122. package/dist/views/streams/dmStreams.js.map +1 -0
  123. package/dist/views/streams/gdmStreams.d.ts +16 -0
  124. package/dist/views/streams/gdmStreams.d.ts.map +1 -0
  125. package/dist/views/streams/gdmStreams.js +40 -0
  126. package/dist/views/streams/gdmStreams.js.map +1 -0
  127. package/dist/views/streams/spaceStreams.d.ts +17 -0
  128. package/dist/views/streams/spaceStreams.d.ts.map +1 -0
  129. package/dist/views/streams/spaceStreams.js +52 -0
  130. package/dist/views/streams/spaceStreams.js.map +1 -0
  131. package/dist/views/streams/streamMemberIds.d.ts +8 -0
  132. package/dist/views/streams/streamMemberIds.d.ts.map +1 -0
  133. package/dist/views/streams/streamMemberIds.js +34 -0
  134. package/dist/views/streams/streamMemberIds.js.map +1 -0
  135. package/dist/views/streams/streamStatus.d.ts +3 -2
  136. package/dist/views/streams/streamStatus.d.ts.map +1 -1
  137. package/dist/views/streams/streamStatus.js +11 -9
  138. package/dist/views/streams/streamStatus.js.map +1 -1
  139. package/dist/views/streams/timelineEvents.d.ts +1 -1
  140. package/dist/views/streams/timelines.d.ts +1 -1
  141. package/dist/views/streams/unreadMarkersTransform.d.ts +1 -1
  142. package/dist/views/streams/unreadMarkersTransform.d.ts.map +1 -1
  143. package/dist/views/streams/unreadMarkersTransform.js.map +1 -1
  144. package/dist/views/streams/userInboxStreams.d.ts +8 -0
  145. package/dist/views/streams/userInboxStreams.d.ts.map +1 -0
  146. package/dist/views/streams/userInboxStreams.js +11 -0
  147. package/dist/views/streams/userInboxStreams.js.map +1 -0
  148. package/dist/views/streams/userMetadataStreams.d.ts +8 -0
  149. package/dist/views/streams/userMetadataStreams.d.ts.map +1 -0
  150. package/dist/views/streams/userMetadataStreams.js +12 -0
  151. package/dist/views/streams/userMetadataStreams.js.map +1 -0
  152. package/dist/views/streams/userSettingsStreams.d.ts +6 -5
  153. package/dist/views/streams/userSettingsStreams.d.ts.map +1 -1
  154. package/dist/views/streams/userSettingsStreams.js +29 -17
  155. package/dist/views/streams/userSettingsStreams.js.map +1 -1
  156. package/dist/views/streams/userStreamsView.d.ts +26 -0
  157. package/dist/views/streams/userStreamsView.d.ts.map +1 -0
  158. package/dist/views/streams/userStreamsView.js +106 -0
  159. package/dist/views/streams/userStreamsView.js.map +1 -0
  160. package/dist/views/streamsView.d.ts +28 -2
  161. package/dist/views/streamsView.d.ts.map +1 -1
  162. package/dist/views/streamsView.js +71 -10
  163. package/dist/views/streamsView.js.map +1 -1
  164. package/dist/views/transforms/dmsAndGdmsTransform.d.ts +24 -0
  165. package/dist/views/transforms/dmsAndGdmsTransform.d.ts.map +1 -0
  166. package/dist/views/transforms/dmsAndGdmsTransform.js +63 -0
  167. package/dist/views/transforms/dmsAndGdmsTransform.js.map +1 -0
  168. package/dist/views/transforms/membershipsTransform.d.ts +4 -0
  169. package/dist/views/transforms/membershipsTransform.d.ts.map +1 -0
  170. package/dist/views/transforms/membershipsTransform.js +16 -0
  171. package/dist/views/transforms/membershipsTransform.js.map +1 -0
  172. package/dist/views/transforms/spaceIdsTransform.d.ts +3 -0
  173. package/dist/views/transforms/spaceIdsTransform.d.ts.map +1 -0
  174. package/dist/views/transforms/spaceIdsTransform.js +14 -0
  175. package/dist/views/transforms/spaceIdsTransform.js.map +1 -0
  176. package/package.json +9 -9
@@ -0,0 +1,678 @@
1
+ import { SessionKeysSchema, } from '@towns-protocol/proto';
2
+ import { shortenHexString, dlog, dlogError, check, bin_toHexString, } from '@towns-protocol/dlog';
3
+ import { GroupEncryptionAlgorithmId, parseGroupEncryptionAlgorithmId, } from '@towns-protocol/encryption';
4
+ import { create, fromJsonString } from '@bufbuild/protobuf';
5
+ import { sortedArraysEqual } from './observable/utils';
6
+ export var DecryptionStatus;
7
+ (function (DecryptionStatus) {
8
+ DecryptionStatus["initializing"] = "initializing";
9
+ DecryptionStatus["updating"] = "updating";
10
+ DecryptionStatus["working"] = "working";
11
+ DecryptionStatus["idle"] = "idle";
12
+ DecryptionStatus["done"] = "done";
13
+ })(DecryptionStatus || (DecryptionStatus = {}));
14
+ class StreamTasks {
15
+ encryptedContent = new Array();
16
+ keySolicitations = new Array();
17
+ isMissingKeys = false;
18
+ keySolicitationsNeedsSort = false;
19
+ sortKeySolicitations() {
20
+ this.keySolicitations.sort((a, b) => a.respondAfter - b.respondAfter);
21
+ this.keySolicitationsNeedsSort = false;
22
+ }
23
+ isEmpty() {
24
+ return (this.encryptedContent.length === 0 &&
25
+ this.keySolicitations.length === 0 &&
26
+ !this.isMissingKeys);
27
+ }
28
+ }
29
+ class StreamQueues {
30
+ streams = new Map();
31
+ getStreamIds() {
32
+ return Array.from(this.streams.keys());
33
+ }
34
+ getQueue(streamId) {
35
+ let tasks = this.streams.get(streamId);
36
+ if (!tasks) {
37
+ tasks = new StreamTasks();
38
+ this.streams.set(streamId, tasks);
39
+ }
40
+ return tasks;
41
+ }
42
+ isEmpty() {
43
+ for (const tasks of this.streams.values()) {
44
+ if (!tasks.isEmpty()) {
45
+ return false;
46
+ }
47
+ }
48
+ return true;
49
+ }
50
+ toString() {
51
+ const counts = Array.from(this.streams.entries()).reduce((acc, [_, stream]) => {
52
+ acc['encryptedContent'] =
53
+ (acc['encryptedContent'] ?? 0) + stream.encryptedContent.length;
54
+ acc['streamsMissingKeys'] =
55
+ (acc['streamsMissingKeys'] ?? 0) + (stream.isMissingKeys ? 1 : 0);
56
+ acc['keySolicitations'] =
57
+ (acc['keySolicitations'] ?? 0) + stream.keySolicitations.length;
58
+ return acc;
59
+ }, {});
60
+ return Object.entries(counts)
61
+ .map(([key, count]) => `${key}: ${count}`)
62
+ .join(', ');
63
+ }
64
+ }
65
+ /**
66
+ *
67
+ * Responsibilities:
68
+ * 1. Download new to-device messages that happened while we were offline
69
+ * 2. Decrypt new to-device messages
70
+ * 3. Decrypt encrypted content
71
+ * 4. Retry decryption failures, request keys for failed decryption
72
+ * 5. Respond to key solicitations
73
+ *
74
+ *
75
+ * Notes:
76
+ * If in the future we started snapshotting the eventNum of the last message sent by every user,
77
+ * we could use that to determine the order we send out keys, and the order that we reply to key solicitations.
78
+ *
79
+ * It should be easy to introduce a priority stream, where we decrypt messages from that stream first, before
80
+ * anything else, so the messages show up quicky in the ui that the user is looking at.
81
+ *
82
+ * We need code to purge bad sessions (if someones sends us the wrong key, or a key that doesn't decrypt the message)
83
+ */
84
+ export class BaseDecryptionExtensions {
85
+ _status = DecryptionStatus.initializing;
86
+ mainQueues = {
87
+ priorityTasks: new Array(),
88
+ newGroupSession: new Array(),
89
+ ownKeySolicitations: new Array(),
90
+ };
91
+ streamQueues = new StreamQueues();
92
+ upToDateStreams = new Set();
93
+ highPriorityIds = new Set();
94
+ recentStreamIds = [];
95
+ decryptionFailures = {}; // streamId: sessionId: EncryptedContentItem[]
96
+ inProgressTick;
97
+ timeoutId;
98
+ delayMs = 1;
99
+ started = false;
100
+ numRecentStreamIds = 5;
101
+ emitter;
102
+ _onStopFn;
103
+ log;
104
+ crypto;
105
+ entitlementDelegate;
106
+ userDevice;
107
+ userId;
108
+ constructor(emitter, crypto, entitlementDelegate, userDevice, userId, upToDateStreams, inLogId) {
109
+ this.emitter = emitter;
110
+ this.crypto = crypto;
111
+ this.entitlementDelegate = entitlementDelegate;
112
+ this.userDevice = userDevice;
113
+ this.userId = userId;
114
+ // initialize with a set of up-to-date streams
115
+ // ready for processing
116
+ this.upToDateStreams = upToDateStreams;
117
+ const shortKey = shortenHexString(userDevice.deviceKey);
118
+ const logId = `${inLogId}:${shortKey}`;
119
+ this.log = {
120
+ debug: dlog('csb:decryption:debug', { defaultEnabled: false }).extend(logId),
121
+ info: dlog('csb:decryption', { defaultEnabled: true }).extend(logId),
122
+ error: dlogError('csb:decryption:error').extend(logId),
123
+ };
124
+ this.log.debug('new DecryptionExtensions', { userDevice });
125
+ }
126
+ enqueueNewGroupSessions(sessions, _senderId) {
127
+ this.log.debug('enqueueNewGroupSessions', sessions);
128
+ const streamId = bin_toHexString(sessions.streamId);
129
+ this.mainQueues.newGroupSession.push({ streamId, sessions });
130
+ this.checkStartTicking();
131
+ }
132
+ enqueueNewEncryptedContent(streamId, eventId, kind, // kind of encrypted data
133
+ encryptedData) {
134
+ // dms, channels, gdms ("we're in the wrong package")
135
+ if (streamId.startsWith('20') || streamId.startsWith('88') || streamId.startsWith('77')) {
136
+ this.recentStreamIds.push(streamId);
137
+ if (this.recentStreamIds.length > this.numRecentStreamIds) {
138
+ this.recentStreamIds.shift();
139
+ }
140
+ }
141
+ this.streamQueues.getQueue(streamId).encryptedContent.push({
142
+ streamId,
143
+ eventId,
144
+ kind,
145
+ encryptedData,
146
+ });
147
+ this.checkStartTicking();
148
+ }
149
+ enqueueInitKeySolicitations(streamId, eventHashStr, members, sigBundle) {
150
+ const streamQueue = this.streamQueues.getQueue(streamId);
151
+ streamQueue.keySolicitations = [];
152
+ this.mainQueues.ownKeySolicitations = this.mainQueues.ownKeySolicitations.filter((x) => x.streamId !== streamId);
153
+ for (const member of members) {
154
+ const { userId: fromUserId, userAddress: fromUserAddress } = member;
155
+ for (const keySolicitation of member.solicitations) {
156
+ if (keySolicitation.deviceKey === this.userDevice.deviceKey) {
157
+ continue;
158
+ }
159
+ if (keySolicitation.sessionIds.length === 0) {
160
+ continue;
161
+ }
162
+ const selectedQueue = fromUserId === this.userId
163
+ ? this.mainQueues.ownKeySolicitations
164
+ : streamQueue.keySolicitations;
165
+ selectedQueue.push({
166
+ streamId,
167
+ fromUserId,
168
+ fromUserAddress,
169
+ solicitation: keySolicitation,
170
+ respondAfter: Date.now() + this.getRespondDelayMSForKeySolicitation(streamId, fromUserId),
171
+ sigBundle,
172
+ hashStr: eventHashStr,
173
+ });
174
+ }
175
+ }
176
+ streamQueue.keySolicitationsNeedsSort = true;
177
+ this.checkStartTicking();
178
+ }
179
+ enqueueKeySolicitation(streamId, eventHashStr, fromUserId, fromUserAddress, keySolicitation, sigBundle) {
180
+ if (keySolicitation.deviceKey === this.userDevice.deviceKey) {
181
+ //this.log.debug('ignoring key solicitation for our own device')
182
+ return;
183
+ }
184
+ const streamQueue = this.streamQueues.getQueue(streamId);
185
+ const selectedQueue = fromUserId === this.userId
186
+ ? this.mainQueues.ownKeySolicitations
187
+ : streamQueue.keySolicitations;
188
+ const index = selectedQueue.findIndex((x) => x.streamId === streamId && x.solicitation.deviceKey === keySolicitation.deviceKey);
189
+ if (index > -1) {
190
+ selectedQueue.splice(index, 1);
191
+ }
192
+ if (keySolicitation.sessionIds.length > 0 || keySolicitation.isNewDevice) {
193
+ //this.log.debug('new key solicitation', { fromUserId, streamId, keySolicitation })
194
+ streamQueue.keySolicitationsNeedsSort = true;
195
+ selectedQueue.push({
196
+ streamId,
197
+ fromUserId,
198
+ fromUserAddress,
199
+ solicitation: keySolicitation,
200
+ respondAfter: Date.now() + this.getRespondDelayMSForKeySolicitation(streamId, fromUserId),
201
+ sigBundle,
202
+ hashStr: eventHashStr,
203
+ });
204
+ this.checkStartTicking();
205
+ }
206
+ else if (index > -1) {
207
+ //this.log.debug('cleared key solicitation', keySolicitation)
208
+ }
209
+ }
210
+ setStreamUpToDate(streamId) {
211
+ //this.log.debug('streamUpToDate', streamId)
212
+ this.upToDateStreams.add(streamId);
213
+ this.checkStartTicking();
214
+ }
215
+ resetUpToDateStreams() {
216
+ this.upToDateStreams.clear();
217
+ this.checkStartTicking();
218
+ }
219
+ retryDecryptionFailures(streamId) {
220
+ const streamQueue = this.streamQueues.getQueue(streamId);
221
+ if (this.decryptionFailures[streamId] &&
222
+ Object.keys(this.decryptionFailures[streamId]).length > 0) {
223
+ this.log.debug('membership change, re-enqueuing decryption failures for stream', streamId);
224
+ streamQueue.isMissingKeys = true;
225
+ this.checkStartTicking();
226
+ }
227
+ }
228
+ start() {
229
+ check(!this.started, 'start() called twice, please re-instantiate instead');
230
+ this.log.debug('starting');
231
+ this.started = true;
232
+ // let the subclass override and do any custom startup tasks
233
+ this.onStart();
234
+ // enqueue a task to upload device keys
235
+ this.mainQueues.priorityTasks.push(() => this.uploadDeviceKeys());
236
+ // enqueue a task to download new to-device messages
237
+ this.enqueueNewMessageDownload();
238
+ // start the tick loop
239
+ this.checkStartTicking();
240
+ }
241
+ // enqueue a task to download new to-device messages, should be safe to call multiple times
242
+ enqueueNewMessageDownload() {
243
+ this.mainQueues.priorityTasks.push(() => this.downloadNewMessages());
244
+ }
245
+ onStart() {
246
+ // let the subclass override and do any custom startup tasks
247
+ }
248
+ async stop() {
249
+ this._onStopFn?.();
250
+ this._onStopFn = undefined;
251
+ // let the subclass override and do any custom shutdown tasks
252
+ await this.onStop();
253
+ await this.stopTicking();
254
+ }
255
+ onStop() {
256
+ // let the subclass override and do any custom shutdown tasks
257
+ return Promise.resolve();
258
+ }
259
+ get status() {
260
+ return this._status;
261
+ }
262
+ setStatus(status) {
263
+ if (this._status !== status) {
264
+ this.log.debug(`status changed ${status}`);
265
+ this._status = status;
266
+ this.emitter.emit('decryptionExtStatusChanged', status);
267
+ }
268
+ }
269
+ compareStreamIds(a, b) {
270
+ const recentStreamIds = new Set(this.recentStreamIds);
271
+ return (this.getPriorityForStream(a, this.highPriorityIds, recentStreamIds) -
272
+ this.getPriorityForStream(b, this.highPriorityIds, recentStreamIds));
273
+ }
274
+ lastPrintedAt = 0;
275
+ checkStartTicking() {
276
+ if (!this.started ||
277
+ this.timeoutId ||
278
+ !this._onStopFn ||
279
+ !this.isUserInboxStreamUpToDate(this.upToDateStreams) ||
280
+ this.shouldPauseTicking()) {
281
+ return;
282
+ }
283
+ if (!Object.values(this.mainQueues).find((q) => q.length > 0) &&
284
+ this.streamQueues.isEmpty()) {
285
+ this.setStatus(DecryptionStatus.done);
286
+ return;
287
+ }
288
+ if (Date.now() - this.lastPrintedAt > 30000) {
289
+ this.log.info(`status: ${this.status} queues: ${Object.entries(this.mainQueues)
290
+ .map(([key, q]) => `${key}: ${q.length}`)
291
+ .join(', ')} ${this.streamQueues.toString()}`);
292
+ const streamIds = Array.from(this.streamQueues.streams.entries())
293
+ .filter(([_, value]) => !value.isEmpty())
294
+ .map(([key, _]) => key)
295
+ .sort((a, b) => this.compareStreamIds(a, b));
296
+ const first4Priority = streamIds
297
+ .filter((x) => this.upToDateStreams.has(x))
298
+ .slice(0, 4)
299
+ .join(', ');
300
+ const first4Blocked = streamIds
301
+ .filter((x) => !this.upToDateStreams.has(x))
302
+ .slice(0, 4)
303
+ .join(', ');
304
+ if (first4Priority.length > 0 || first4Blocked.length > 0) {
305
+ this.log.info(`priorityTasks: ${first4Priority} waitingFor: ${first4Blocked}`);
306
+ }
307
+ this.lastPrintedAt = Date.now();
308
+ }
309
+ this.timeoutId = setTimeout(() => {
310
+ this.inProgressTick = this.tick();
311
+ this.inProgressTick
312
+ .catch((e) => this.log.error('ProcessTick Error', e))
313
+ .finally(() => {
314
+ this.timeoutId = undefined;
315
+ setTimeout(() => this.checkStartTicking());
316
+ });
317
+ }, this.getDelayMs());
318
+ }
319
+ async stopTicking() {
320
+ if (this.timeoutId) {
321
+ clearTimeout(this.timeoutId);
322
+ this.timeoutId = undefined;
323
+ }
324
+ if (this.inProgressTick) {
325
+ try {
326
+ await this.inProgressTick;
327
+ }
328
+ catch (e) {
329
+ this.log.error('ProcessTick Error while stopping', e);
330
+ }
331
+ finally {
332
+ this.inProgressTick = undefined;
333
+ }
334
+ }
335
+ }
336
+ getDelayMs() {
337
+ if (this.mainQueues.newGroupSession.length > 0) {
338
+ return 0;
339
+ }
340
+ else {
341
+ return this.delayMs;
342
+ }
343
+ }
344
+ // just do one thing then return
345
+ tick() {
346
+ const now = Date.now();
347
+ const priorityTask = this.mainQueues.priorityTasks.shift();
348
+ if (priorityTask) {
349
+ this.setStatus(DecryptionStatus.updating);
350
+ return priorityTask();
351
+ }
352
+ // update any new group sessions
353
+ const session = this.mainQueues.newGroupSession.shift();
354
+ if (session) {
355
+ this.setStatus(DecryptionStatus.working);
356
+ return this.processNewGroupSession(session);
357
+ }
358
+ const ownSolicitation = this.mainQueues.ownKeySolicitations.shift();
359
+ if (ownSolicitation) {
360
+ this.log.debug(' processing own key solicitation');
361
+ this.setStatus(DecryptionStatus.working);
362
+ return this.processKeySolicitation(ownSolicitation);
363
+ }
364
+ const streamIds = this.streamQueues.getStreamIds();
365
+ streamIds.sort((a, b) => this.compareStreamIds(a, b));
366
+ for (const streamId of streamIds) {
367
+ if (!this.upToDateStreams.has(streamId)) {
368
+ continue;
369
+ }
370
+ const streamQueue = this.streamQueues.getQueue(streamId);
371
+ const encryptedContent = streamQueue.encryptedContent.shift();
372
+ if (encryptedContent) {
373
+ this.setStatus(DecryptionStatus.working);
374
+ return this.processEncryptedContentItem(encryptedContent);
375
+ }
376
+ if (streamQueue.isMissingKeys) {
377
+ this.setStatus(DecryptionStatus.working);
378
+ streamQueue.isMissingKeys = false;
379
+ return this.processMissingKeys(streamId);
380
+ }
381
+ if (streamQueue.keySolicitationsNeedsSort) {
382
+ streamQueue.sortKeySolicitations();
383
+ }
384
+ const keySolicitation = dequeueUpToDate(streamQueue.keySolicitations, now, (x) => x.respondAfter, this.upToDateStreams);
385
+ if (keySolicitation) {
386
+ this.setStatus(DecryptionStatus.working);
387
+ return this.processKeySolicitation(keySolicitation);
388
+ }
389
+ }
390
+ this.setStatus(DecryptionStatus.idle);
391
+ return Promise.resolve();
392
+ }
393
+ /**
394
+ * processNewGroupSession
395
+ * process new group sessions that were sent to our to device stream inbox
396
+ * re-enqueue any decryption failures with matching session id
397
+ */
398
+ async processNewGroupSession(sessionItem) {
399
+ const { streamId, sessions: session } = sessionItem;
400
+ // check if this message is to our device
401
+ const ciphertext = session.ciphertexts[this.userDevice.deviceKey];
402
+ if (!ciphertext) {
403
+ this.log.debug('skipping, no session for our device');
404
+ return;
405
+ }
406
+ this.log.debug('processNewGroupSession', session);
407
+ // check if it contains any keys we need, default to GroupEncryption if the algorithm is not set
408
+ const parsed = parseGroupEncryptionAlgorithmId(session.algorithm, GroupEncryptionAlgorithmId.GroupEncryption);
409
+ if (parsed.kind === 'unrecognized') {
410
+ // todo dispatch event to update the error message
411
+ this.log.error('skipping, invalid algorithm', session.algorithm);
412
+ return;
413
+ }
414
+ const algorithm = parsed.value;
415
+ const neededKeyIndexs = [];
416
+ for (let i = 0; i < session.sessionIds.length; i++) {
417
+ const sessionId = session.sessionIds[i];
418
+ const hasKeys = await this.crypto.hasSessionKey(streamId, sessionId, algorithm);
419
+ if (!hasKeys) {
420
+ neededKeyIndexs.push(i);
421
+ }
422
+ }
423
+ if (!neededKeyIndexs.length) {
424
+ this.log.debug('skipping, we have all the keys');
425
+ return;
426
+ }
427
+ // decrypt the message
428
+ const cleartext = await this.crypto.decryptWithDeviceKey(ciphertext, session.senderKey);
429
+ const sessionKeys = fromJsonString(SessionKeysSchema, cleartext);
430
+ check(sessionKeys.keys.length === session.sessionIds.length, 'bad sessionKeys');
431
+ // make group sessions
432
+ const sessions = neededKeyIndexs.map((i) => ({
433
+ streamId: streamId,
434
+ sessionId: session.sessionIds[i],
435
+ sessionKey: sessionKeys.keys[i],
436
+ algorithm: algorithm,
437
+ }));
438
+ // import the sessions
439
+ this.log.debug('importing group sessions streamId:', streamId, 'count: ', sessions.length, session.sessionIds);
440
+ try {
441
+ await this.crypto.importSessionKeys(streamId, sessions);
442
+ // re-enqueue any decryption failures with these ids
443
+ const streamQueue = this.streamQueues.getQueue(streamId);
444
+ for (const session of sessions) {
445
+ if (this.decryptionFailures[streamId]?.[session.sessionId]) {
446
+ streamQueue.encryptedContent.push(...this.decryptionFailures[streamId][session.sessionId]);
447
+ delete this.decryptionFailures[streamId][session.sessionId];
448
+ }
449
+ }
450
+ }
451
+ catch (e) {
452
+ // don't re-enqueue to prevent infinite loops if this session is truely corrupted
453
+ // we will keep requesting it on each boot until it goes out of the scroll window
454
+ this.log.error('failed to import sessions', { sessionItem, error: e });
455
+ }
456
+ // if we processed them all, ack the stream
457
+ if (this.mainQueues.newGroupSession.length === 0) {
458
+ await this.ackNewGroupSession(session);
459
+ }
460
+ }
461
+ /**
462
+ * processEncryptedContentItem
463
+ * try to decrypt encrytped content
464
+ */
465
+ async processEncryptedContentItem(item) {
466
+ this.log.debug('processEncryptedContentItem', item);
467
+ try {
468
+ await this.decryptGroupEvent(item.streamId, item.eventId, item.kind, item.encryptedData);
469
+ }
470
+ catch (err) {
471
+ const sessionNotFound = isSessionNotFoundError(err);
472
+ this.onDecryptionError(item, {
473
+ missingSession: sessionNotFound,
474
+ kind: item.kind,
475
+ encryptedData: item.encryptedData,
476
+ error: err,
477
+ });
478
+ if (sessionNotFound) {
479
+ const streamId = item.streamId;
480
+ const sessionId = item.encryptedData.sessionId && item.encryptedData.sessionId.length > 0
481
+ ? item.encryptedData.sessionId
482
+ : bin_toHexString(item.encryptedData.sessionIdBytes);
483
+ if (sessionId.length === 0) {
484
+ this.log.error('session id length is 0 for failed decryption', {
485
+ err,
486
+ streamId: item.streamId,
487
+ eventId: item.eventId,
488
+ });
489
+ return;
490
+ }
491
+ if (!this.decryptionFailures[streamId]) {
492
+ this.decryptionFailures[streamId] = { [sessionId]: [item] };
493
+ }
494
+ else if (!this.decryptionFailures[streamId][sessionId]) {
495
+ this.decryptionFailures[streamId][sessionId] = [item];
496
+ }
497
+ else if (!this.decryptionFailures[streamId][sessionId].includes(item)) {
498
+ this.decryptionFailures[streamId][sessionId].push(item);
499
+ }
500
+ const streamQueue = this.streamQueues.getQueue(streamId);
501
+ streamQueue.isMissingKeys = true;
502
+ }
503
+ else {
504
+ this.log.info('failed to decrypt', err, 'streamId', item.streamId);
505
+ }
506
+ }
507
+ }
508
+ /**
509
+ * processMissingKeys
510
+ * process missing keys and send key solicitations to streams
511
+ */
512
+ async processMissingKeys(streamId) {
513
+ this.log.debug('processing missing keys', streamId);
514
+ const missingSessionIds = takeFirst(100, Object.keys(this.decryptionFailures[streamId] ?? {}).sort());
515
+ // limit to 100 keys for now todo revisit https://linear.app/hnt-labs/issue/HNT-3936/revisit-how-we-limit-the-number-of-session-ids-that-we-request
516
+ if (!missingSessionIds.length) {
517
+ this.log.debug('processing missing keys', streamId, 'no missing keys');
518
+ return;
519
+ }
520
+ if (!this.hasStream(streamId)) {
521
+ this.log.debug('processing missing keys', streamId, 'stream not found');
522
+ return;
523
+ }
524
+ const isEntitled = await this.isUserEntitledToKeyExchange(streamId, this.userId, {
525
+ skipOnChainValidation: true,
526
+ });
527
+ if (!isEntitled) {
528
+ this.log.debug('processing missing keys', streamId, 'user is not member of stream');
529
+ return;
530
+ }
531
+ const solicitedEvents = this.getKeySolicitations(streamId);
532
+ const existingKeyRequest = solicitedEvents.find((x) => x.deviceKey === this.userDevice.deviceKey);
533
+ if (existingKeyRequest?.isNewDevice ||
534
+ sortedArraysEqual(existingKeyRequest?.sessionIds ?? [], missingSessionIds)) {
535
+ this.log.debug('processing missing keys already requested keys for this session', existingKeyRequest);
536
+ return;
537
+ }
538
+ const knownSessionIds = await this.crypto.getGroupSessionIds(streamId);
539
+ const isNewDevice = knownSessionIds.length === 0;
540
+ this.log.debug('requesting keys', streamId, 'isNewDevice', isNewDevice, 'sessionIds:', missingSessionIds.length);
541
+ await this.sendKeySolicitation({
542
+ streamId,
543
+ isNewDevice,
544
+ missingSessionIds,
545
+ });
546
+ }
547
+ /**
548
+ * processKeySolicitation
549
+ * process incoming key solicitations and send keys and key fulfillments
550
+ */
551
+ async processKeySolicitation(item) {
552
+ this.log.debug('processing key solicitation', item.streamId, item);
553
+ const streamId = item.streamId;
554
+ check(this.hasStream(streamId), 'stream not found');
555
+ const { isValid, reason } = this.isValidEvent(item);
556
+ if (!isValid) {
557
+ this.log.error('processing key solicitation: invalid event id', {
558
+ streamId,
559
+ eventId: item.hashStr,
560
+ reason,
561
+ });
562
+ return;
563
+ }
564
+ const knownSessionIds = await this.crypto.getGroupSessionIds(streamId);
565
+ // todo split this up by algorithm so that we can send all the new hybrid keys
566
+ knownSessionIds.sort();
567
+ const requestedSessionIds = new Set(item.solicitation.sessionIds.sort());
568
+ const replySessionIds = item.solicitation.isNewDevice
569
+ ? knownSessionIds
570
+ : knownSessionIds.filter((x) => requestedSessionIds.has(x));
571
+ if (replySessionIds.length === 0) {
572
+ this.log.debug('processing key solicitation: no keys to reply with');
573
+ return;
574
+ }
575
+ const isUserEntitledToKeyExchange = await this.isUserEntitledToKeyExchange(streamId, item.fromUserId);
576
+ if (!isUserEntitledToKeyExchange) {
577
+ return;
578
+ }
579
+ const allSessions = [];
580
+ for (const sessionId of replySessionIds) {
581
+ const groupSession = await this.crypto.exportGroupSession(streamId, sessionId);
582
+ if (groupSession) {
583
+ allSessions.push(groupSession);
584
+ }
585
+ }
586
+ this.log.debug('processing key solicitation with', item.streamId, {
587
+ to: item.fromUserId,
588
+ toDevice: item.solicitation.deviceKey,
589
+ requestedCount: item.solicitation.sessionIds.length,
590
+ replyIds: replySessionIds.length,
591
+ sessions: allSessions.length,
592
+ });
593
+ if (allSessions.length === 0) {
594
+ return;
595
+ }
596
+ // send a single key fulfillment for all algorithms
597
+ const { error } = await this.sendKeyFulfillment({
598
+ streamId,
599
+ userAddress: item.fromUserAddress,
600
+ deviceKey: item.solicitation.deviceKey,
601
+ sessionIds: allSessions
602
+ .map((x) => x.sessionId)
603
+ .filter((x) => requestedSessionIds.has(x))
604
+ .sort(),
605
+ });
606
+ // if the key fulfillment failed, someone else already sent a key fulfillment
607
+ if (error) {
608
+ if (!error.msg.includes('DUPLICATE_EVENT') && !error.msg.includes('NOT_FOUND')) {
609
+ // duplicate events are expected, we can ignore them, others are not
610
+ this.log.error('failed to send key fulfillment', error);
611
+ }
612
+ return;
613
+ }
614
+ // if the key fulfillment succeeded, send one group session payload for each algorithm
615
+ const sessions = allSessions.reduce((acc, session) => {
616
+ if (!acc[session.algorithm]) {
617
+ acc[session.algorithm] = [];
618
+ }
619
+ acc[session.algorithm].push(session);
620
+ return acc;
621
+ }, {});
622
+ // send one key fulfillment for each algorithm
623
+ for (const kv of Object.entries(sessions)) {
624
+ const algorithm = kv[0];
625
+ const sessions = kv[1];
626
+ await this.encryptAndShareGroupSessions({
627
+ streamId,
628
+ item,
629
+ sessions,
630
+ algorithm,
631
+ });
632
+ }
633
+ }
634
+ /**
635
+ * can be overridden to add a delay to the key solicitation response
636
+ */
637
+ getRespondDelayMSForKeySolicitation(_streamId, _userId) {
638
+ return 0;
639
+ }
640
+ setHighPriorityStreams(streamIds) {
641
+ this.highPriorityIds = new Set(streamIds);
642
+ }
643
+ }
644
+ export function makeSessionKeys(sessions) {
645
+ const sessionKeys = sessions.map((s) => s.sessionKey);
646
+ return create(SessionKeysSchema, {
647
+ keys: sessionKeys,
648
+ });
649
+ }
650
+ /// Returns the first item from the array,
651
+ /// if dateFn is provided, returns the first item where dateFn(item) <= now
652
+ function dequeueUpToDate(items, now, dateFn, upToDateStreams) {
653
+ if (items.length === 0) {
654
+ return undefined;
655
+ }
656
+ if (dateFn(items[0]) > now) {
657
+ return undefined;
658
+ }
659
+ const index = items.findIndex((x) => dateFn(x) <= now && upToDateStreams.has(x.streamId));
660
+ if (index === -1) {
661
+ return undefined;
662
+ }
663
+ return items.splice(index, 1)[0];
664
+ }
665
+ function takeFirst(count, array) {
666
+ const result = [];
667
+ for (let i = 0; i < count && i < array.length; i++) {
668
+ result.push(array[i]);
669
+ }
670
+ return result;
671
+ }
672
+ function isSessionNotFoundError(err) {
673
+ if (err !== null && typeof err === 'object' && 'message' in err) {
674
+ return err.message.toLowerCase().includes('session not found');
675
+ }
676
+ return false;
677
+ }
678
+ //# sourceMappingURL=decryptionExtensions.js.map