nostr-double-ratchet 0.0.27 → 0.0.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nostr-double-ratchet",
3
- "version": "0.0.27",
3
+ "version": "0.0.28",
4
4
  "type": "module",
5
5
  "packageManager": "yarn@1.22.22",
6
6
  "description": "Nostr double ratchet library",
@@ -14,7 +14,8 @@
14
14
  "build": "vite build && tsc",
15
15
  "examples": "cd examples && vite build && tsc",
16
16
  "examples-dev": "cd examples && vite && tsc",
17
- "docs": "typedoc --out docs src/index.ts"
17
+ "docs": "typedoc --out docs src/index.ts",
18
+ "lint": "eslint src --ext .ts --fix"
18
19
  },
19
20
  "repository": {
20
21
  "type": "git",
@@ -32,25 +33,26 @@
32
33
  "dist"
33
34
  ],
34
35
  "devDependencies": {
35
- "@nostr-dev-kit/ndk": "2.11.2",
36
- "@types/lodash": "^4.17.15",
37
- "@types/node": "^22.13.4",
38
- "@typescript-eslint/eslint-plugin": "^8.24.1",
39
- "@typescript-eslint/parser": "^8.24.1",
40
- "eslint": "^9.20.1",
41
- "eslint-config-prettier": "^10.0.1",
42
- "eslint-plugin-prettier": "^5.2.3",
36
+ "@nostr-dev-kit/ndk": "2.14.23",
37
+ "@types/lodash": "^4.17.17",
38
+ "@types/node": "^22.15.21",
39
+ "@typescript-eslint/eslint-plugin": "^8.33.1",
40
+ "@typescript-eslint/parser": "^8.33.1",
41
+ "eslint": "^9.28.0",
42
+ "eslint-config-prettier": "^10.1.5",
43
+ "eslint-plugin-prettier": "^5.4.0",
43
44
  "eslint-plugin-simple-import-sort": "^12.1.1",
44
45
  "lodash": "^4.17.21",
45
- "tsx": "^4.19.2",
46
- "typedoc": "^0.27.7",
47
- "typescript": "^5.7.3",
46
+ "tsx": "^4.19.4",
47
+ "typedoc": "^0.28.4",
48
+ "typescript": "^5.8.3",
48
49
  "typescript-lru-cache": "^2.0.0",
49
- "vite": "^6.1.0",
50
- "vitest": "^3.0.6",
51
- "ws": "^8.18.0"
50
+ "vite": "^6.3.5",
51
+ "vitest": "^3.1.4",
52
+ "ws": "^8.18.2"
52
53
  },
53
54
  "dependencies": {
54
- "nostr-tools": "^2.10.4"
55
+ "nostr-tools": "^2.13.0",
56
+ "react-blurhash": "^0.3.0"
55
57
  }
56
58
  }
package/src/Invite.ts CHANGED
@@ -28,7 +28,8 @@ export class Invite {
28
28
  public label?: string,
29
29
  public maxUses?: number,
30
30
  public usedBy: string[] = [],
31
- ) {}
31
+ ) {
32
+ }
32
33
 
33
34
  static createNew(inviter: string, label?: string, maxUses?: number): Invite {
34
35
  if (!inviter) {
@@ -55,7 +56,7 @@ export class Invite {
55
56
  }
56
57
 
57
58
  const decodedHash = decodeURIComponent(rawHash);
58
- let data: any;
59
+ let data: { inviter?: string; ephemeralKey?: string; sharedSecret?: string };
59
60
  try {
60
61
  data = JSON.parse(decodedHash);
61
62
  } catch (err) {
@@ -115,12 +116,11 @@ export class Invite {
115
116
  );
116
117
  }
117
118
 
118
- static fromUser(user: string, subscribe: NostrSubscribe, onInvite: (invite: Invite) => void): Unsubscribe {
119
+ static fromUser(user: string, subscribe: NostrSubscribe, onInvite: (_invite: Invite) => void): Unsubscribe {
119
120
  const filter: Filter = {
120
121
  kinds: [INVITE_EVENT_KIND],
121
122
  authors: [user],
122
- limit: 1,
123
- "#d": ["double-ratchet/invites/public"],
123
+ "#l": ["double-ratchet/invites"]
124
124
  };
125
125
  let latest = 0;
126
126
  const unsub = subscribe(filter, (event) => {
@@ -131,8 +131,7 @@ export class Invite {
131
131
  try {
132
132
  const inviteLink = Invite.fromEvent(event);
133
133
  onInvite(inviteLink);
134
- } catch (error) {
135
- console.error("Error processing invite:", error, "event:", event);
134
+ } catch {
136
135
  }
137
136
  });
138
137
 
@@ -168,7 +167,10 @@ export class Invite {
168
167
  return url.toString();
169
168
  }
170
169
 
171
- getEvent(): UnsignedEvent {
170
+ getEvent(deviceName: string): UnsignedEvent {
171
+ if (!deviceName) {
172
+ throw new Error("Device name is required");
173
+ }
172
174
  return {
173
175
  kind: INVITE_EVENT_KIND,
174
176
  pubkey: this.inviter,
@@ -177,7 +179,7 @@ export class Invite {
177
179
  tags: [
178
180
  ['ephemeralKey', this.inviterEphemeralPublicKey],
179
181
  ['sharedSecret', this.sharedSecret],
180
- ['d', 'double-ratchet/invites/public'],
182
+ ['d', 'double-ratchet/invites/' + deviceName],
181
183
  ['l', 'double-ratchet/invites']
182
184
  ],
183
185
  };
@@ -240,7 +242,7 @@ export class Invite {
240
242
  return { session, event: finalizeEvent(envelope, randomSenderKey) };
241
243
  }
242
244
 
243
- listen(decryptor: Uint8Array | DecryptFunction, nostrSubscribe: NostrSubscribe, onSession: (session: Session, identity?: string) => void): Unsubscribe {
245
+ listen(decryptor: Uint8Array | DecryptFunction, nostrSubscribe: NostrSubscribe, onSession: (_session: Session, _identity?: string) => void): Unsubscribe {
244
246
  if (!this.inviterEphemeralPrivateKey) {
245
247
  throw new Error("Inviter session key is not available");
246
248
  }
@@ -253,7 +255,6 @@ export class Invite {
253
255
  return nostrSubscribe(filter, async (event) => {
254
256
  try {
255
257
  if (this.maxUses && this.usedBy.length >= this.maxUses) {
256
- console.error("Invite has reached maximum number of uses");
257
258
  return;
258
259
  }
259
260
 
@@ -279,9 +280,8 @@ export class Invite {
279
280
  const session = Session.init(nostrSubscribe, inviteeSessionPublicKey, this.inviterEphemeralPrivateKey!, false, sharedSecret, name);
280
281
 
281
282
  onSession(session, inviteeIdentity);
282
- } catch (error) {
283
- console.error("Error processing invite message:", error, "event", event);
283
+ } catch {
284
284
  }
285
285
  });
286
286
  }
287
- }
287
+ }
package/src/Session.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { generateSecretKey, getPublicKey, nip44, finalizeEvent, VerifiedEvent, UnsignedEvent, getEventHash, validateEvent } from "nostr-tools";
2
- import { bytesToHex } from "@noble/hashes/utils";
2
+
3
3
  import {
4
4
  SessionState,
5
5
  Header,
@@ -38,7 +38,7 @@ export class Session {
38
38
 
39
39
  /**
40
40
  * Initializes a new secure communication session
41
- * @param nostrSubscribe Function to subscribe to Nostr events
41
+ * @param nostrSubscribe Function to subscribe to Nostr events. Make sure it deduplicates events (doesnt return the same event twice), otherwise you'll see decryption errors!
42
42
  * @param theirNextNostrPublicKey The public key of the other party
43
43
  * @param ourCurrentPrivateKey Our current private key for Nostr
44
44
  * @param isInitiator Whether we are initiating the conversation (true) or responding (false)
@@ -205,17 +205,7 @@ export class Session {
205
205
  this.state.receivingChainKey = newReceivingChainKey;
206
206
  this.state.receivingChainMessageNumber++;
207
207
 
208
- try {
209
- return nip44.decrypt(ciphertext, messageKey);
210
- } catch (error) {
211
- console.error(this.name, 'Decryption failed:', error, {
212
- messageKey: bytesToHex(messageKey).slice(0, 4),
213
- receivingChainKey: bytesToHex(this.state.receivingChainKey).slice(0, 4),
214
- sendingChainKey: this.state.sendingChainKey && bytesToHex(this.state.sendingChainKey).slice(0, 4),
215
- rootKey: bytesToHex(this.state.rootKey).slice(0, 4)
216
- });
217
- throw error;
218
- }
208
+ return nip44.decrypt(ciphertext, messageKey);
219
209
  }
220
210
 
221
211
  private ratchetStep() {
@@ -294,14 +284,14 @@ export class Session {
294
284
  }
295
285
 
296
286
  // 4. NOSTR EVENT HANDLING
297
- private decryptHeader(event: any): [Header, boolean, boolean] {
287
+ private decryptHeader(event: { tags: string[][]; pubkey: string }): [Header, boolean, boolean] {
298
288
  const encryptedHeader = event.tags[0][1];
299
289
  if (this.state.ourCurrentNostrKey) {
300
290
  const currentSecret = nip44.getConversationKey(this.state.ourCurrentNostrKey.privateKey, event.pubkey);
301
291
  try {
302
292
  const header = JSON.parse(nip44.decrypt(encryptedHeader, currentSecret)) as Header;
303
293
  return [header, false, false];
304
- } catch (error) {
294
+ } catch {
305
295
  // Decryption with currentSecret failed, try with nextSecret
306
296
  }
307
297
  }
@@ -310,7 +300,7 @@ export class Session {
310
300
  try {
311
301
  const header = JSON.parse(nip44.decrypt(encryptedHeader, nextSecret)) as Header;
312
302
  return [header, true, false];
313
- } catch (error) {
303
+ } catch {
314
304
  // Decryption with nextSecret also failed
315
305
  }
316
306
 
@@ -320,7 +310,7 @@ export class Session {
320
310
  try {
321
311
  const header = JSON.parse(nip44.decrypt(encryptedHeader, key)) as Header;
322
312
  return [header, false, true];
323
- } catch (error) {
313
+ } catch {
324
314
  // Decryption failed, try next secret
325
315
  }
326
316
  }
@@ -329,7 +319,7 @@ export class Session {
329
319
  throw new Error("Failed to decrypt header with current and skipped header keys");
330
320
  }
331
321
 
332
- private handleNostrEvent(e: any) {
322
+ private handleNostrEvent(e: { tags: string[][]; pubkey: string; content: string }) {
333
323
  const [header, shouldRatchet, isSkipped] = this.decryptHeader(e);
334
324
 
335
325
  if (!isSkipped) {
@@ -358,16 +348,14 @@ export class Session {
358
348
  const text = this.ratchetDecrypt(header, e.content, e.pubkey);
359
349
  const innerEvent = JSON.parse(text);
360
350
  if (!validateEvent(innerEvent)) {
361
- console.error("Invalid event received", innerEvent);
362
351
  return;
363
352
  }
364
353
 
365
354
  if (innerEvent.id !== getEventHash(innerEvent)) {
366
- console.error("Event hash does not match", innerEvent);
367
355
  return;
368
356
  }
369
357
 
370
- this.internalSubscriptions.forEach(callback => callback(innerEvent, e));
358
+ this.internalSubscriptions.forEach(callback => callback(innerEvent, e as VerifiedEvent));
371
359
  }
372
360
 
373
361
  private subscribeToNostrEvents() {
@@ -0,0 +1,165 @@
1
+ import { CHAT_MESSAGE_KIND, NostrPublish, NostrSubscribe, Rumor, Unsubscribe } from "./types"
2
+ import { UserRecord } from "./UserRecord"
3
+ import { Invite } from "./Invite"
4
+ import { getPublicKey } from "nostr-tools"
5
+
6
+ export default class SessionManager {
7
+ private userRecords: Map<string, UserRecord> = new Map()
8
+ private nostrSubscribe: NostrSubscribe
9
+ private nostrPublish: NostrPublish
10
+ private ourIdentityKey: Uint8Array
11
+ private inviteUnsubscribes: Map<string, Unsubscribe> = new Map()
12
+ private ownDeviceInvites: Map<string, Invite | null> = new Map()
13
+
14
+ constructor(ourIdentityKey: Uint8Array, nostrSubscribe: NostrSubscribe, nostrPublish: NostrPublish) {
15
+ this.userRecords = new Map()
16
+ this.nostrSubscribe = nostrSubscribe
17
+ this.nostrPublish = nostrPublish
18
+ this.ourIdentityKey = ourIdentityKey
19
+ }
20
+
21
+ async sendText(recipientIdentityKey: string, text: string) {
22
+ const event = {
23
+ kind: CHAT_MESSAGE_KIND,
24
+ content: text,
25
+ }
26
+ return await this.sendEvent(recipientIdentityKey, event)
27
+ }
28
+
29
+ async sendEvent(recipientIdentityKey: string, event: Partial<Rumor>) {
30
+ const results = []
31
+
32
+ // Send to recipient's devices
33
+ const userRecord = this.userRecords.get(recipientIdentityKey)
34
+ if (!userRecord) {
35
+ // Listen for invites from recipient
36
+ this.listenToUser(recipientIdentityKey)
37
+ throw new Error("No active session with user. Listening for invites.")
38
+ }
39
+
40
+ // Send to all active sessions with recipient
41
+ for (const session of userRecord.getActiveSessions()) {
42
+ const { event: encryptedEvent } = session.sendEvent(event)
43
+ results.push(encryptedEvent)
44
+ }
45
+
46
+ // Send to our own devices (for multi-device sync)
47
+ const ourPublicKey = getPublicKey(this.ourIdentityKey)
48
+ const ownUserRecord = this.userRecords.get(ourPublicKey)
49
+ if (ownUserRecord) {
50
+ for (const session of ownUserRecord.getActiveSessions()) {
51
+ const { event: encryptedEvent } = session.sendEvent(event)
52
+ results.push(encryptedEvent)
53
+ }
54
+ }
55
+
56
+ return results
57
+ }
58
+
59
+ listenToUser(userPubkey: string) {
60
+ // Don't subscribe multiple times to the same user
61
+ if (this.inviteUnsubscribes.has(userPubkey)) return
62
+
63
+ const unsubscribe = Invite.fromUser(userPubkey, this.nostrSubscribe, async (_invite) => {
64
+ try {
65
+ const { session, event } = await _invite.accept(
66
+ this.nostrSubscribe,
67
+ getPublicKey(this.ourIdentityKey),
68
+ this.ourIdentityKey
69
+ )
70
+ this.nostrPublish(event)
71
+
72
+ // Store the new session
73
+ let userRecord = this.userRecords.get(userPubkey)
74
+ if (!userRecord) {
75
+ userRecord = new UserRecord(userPubkey, this.nostrSubscribe)
76
+ this.userRecords.set(userPubkey, userRecord)
77
+ }
78
+ userRecord.insertSession(event.id || 'unknown', session)
79
+
80
+ // Set up event handling for the new session
81
+ session.onEvent((_event) => {
82
+ this.internalSubscriptions.forEach(callback => callback(_event))
83
+ })
84
+
85
+ // Return the event to be published
86
+ return event
87
+ } catch {
88
+ }
89
+ })
90
+
91
+ this.inviteUnsubscribes.set(userPubkey, unsubscribe)
92
+ }
93
+
94
+ stopListeningToUser(userPubkey: string) {
95
+ const unsubscribe = this.inviteUnsubscribes.get(userPubkey)
96
+ if (unsubscribe) {
97
+ unsubscribe()
98
+ this.inviteUnsubscribes.delete(userPubkey)
99
+ }
100
+ }
101
+
102
+ // Update onEvent to include internalSubscriptions management
103
+ private internalSubscriptions: Set<(_event: Rumor) => void> = new Set()
104
+
105
+ onEvent(callback: (_event: Rumor) => void) {
106
+ this.internalSubscriptions.add(callback)
107
+
108
+ // Subscribe to existing sessions
109
+ for (const userRecord of this.userRecords.values()) {
110
+ for (const session of userRecord.getActiveSessions()) {
111
+ session.onEvent((_event: Rumor) => {
112
+ callback(_event)
113
+ })
114
+ }
115
+ }
116
+
117
+ // Return unsubscribe function
118
+ return () => {
119
+ this.internalSubscriptions.delete(callback)
120
+ }
121
+ }
122
+
123
+ close() {
124
+ // Clean up all subscriptions
125
+ for (const unsubscribe of this.inviteUnsubscribes.values()) {
126
+ unsubscribe()
127
+ }
128
+ this.inviteUnsubscribes.clear()
129
+
130
+ // Close all sessions
131
+ for (const userRecord of this.userRecords.values()) {
132
+ for (const session of userRecord.getActiveSessions()) {
133
+ session.close()
134
+ }
135
+ }
136
+ this.userRecords.clear()
137
+ this.internalSubscriptions.clear()
138
+ this.ownDeviceInvites.clear()
139
+ }
140
+
141
+ createOwnDeviceInvite(deviceName: string, label?: string, maxUses?: number): Invite {
142
+ const ourPublicKey = getPublicKey(this.ourIdentityKey)
143
+ const invite = Invite.createNew(ourPublicKey, label, maxUses)
144
+ this.ownDeviceInvites.set(deviceName, invite)
145
+ return invite
146
+ }
147
+
148
+ removeOwnDevice(deviceName: string): void {
149
+ this.ownDeviceInvites.set(deviceName, null)
150
+ }
151
+
152
+ getOwnDeviceInvites(): Map<string, Invite | null> {
153
+ return new Map(this.ownDeviceInvites)
154
+ }
155
+
156
+ getActiveOwnDeviceInvites(): Map<string, Invite> {
157
+ const active = new Map<string, Invite>()
158
+ for (const [deviceName, invite] of this.ownDeviceInvites) {
159
+ if (invite !== null) {
160
+ active.set(deviceName, invite)
161
+ }
162
+ }
163
+ return active
164
+ }
165
+ }
package/src/UserRecord.ts CHANGED
@@ -17,11 +17,21 @@ export class UserRecord {
17
17
  private deviceRecords: Map<string, DeviceRecord> = new Map();
18
18
  private isStale: boolean = false;
19
19
  private staleTimestamp?: number;
20
+ /**
21
+ * Temporary store for sessions when the corresponding deviceId is unknown.
22
+ *
23
+ * SessionManager currently operates at a per-user granularity (it is not
24
+ * yet aware of individual devices). Until full Sesame device handling is
25
+ * implemented we keep sessions in this simple list so that
26
+ * SessionManager.getActiveSessions / getAllSessions work as expected.
27
+ */
28
+ private extraSessions: Session[] = [];
20
29
 
21
30
  constructor(
22
- public userId: string,
23
- private nostrSubscribe: NostrSubscribe,
24
- ) {}
31
+ public _userId: string,
32
+ private _nostrSubscribe: NostrSubscribe,
33
+ ) {
34
+ }
25
35
 
26
36
  /**
27
37
  * Adds or updates a device record for this user
@@ -109,7 +119,7 @@ export class UserRecord {
109
119
  if (this.isStale) return [];
110
120
 
111
121
  return Array.from(this.deviceRecords.entries())
112
- .filter(([_, record]) => !record.isStale && record.activeSession)
122
+ .filter(([, record]) => !record.isStale && record.activeSession)
113
123
  .map(([deviceId, record]) => [deviceId, record.activeSession!]);
114
124
  }
115
125
 
@@ -129,7 +139,7 @@ export class UserRecord {
129
139
  }
130
140
 
131
141
  const session = Session.init(
132
- this.nostrSubscribe,
142
+ this._nostrSubscribe,
133
143
  record.publicKey,
134
144
  ourCurrentPrivateKey,
135
145
  isInitiator,
@@ -179,4 +189,52 @@ export class UserRecord {
179
189
  });
180
190
  this.deviceRecords.clear();
181
191
  }
182
- }
192
+
193
+ // ---------------------------------------------------------------------------
194
+ // Helper methods used by SessionManager (WIP):
195
+ // ---------------------------------------------------------------------------
196
+
197
+ /**
198
+ * Add a session without associating it with a specific device.
199
+ * This is mainly used by SessionManager which does not yet keep track of
200
+ * device identifiers. The session will be considered active.
201
+ */
202
+ public addSession(session: Session): void {
203
+ this.extraSessions.push(session);
204
+ }
205
+
206
+ /**
207
+ * Return all sessions that are currently considered *active*.
208
+ * For now this means any session in a non-stale device record as well as
209
+ * all sessions added through `addSession`.
210
+ */
211
+ public getActiveSessions(): Session[] {
212
+ const sessions: Session[] = [...this.extraSessions];
213
+
214
+ for (const record of this.deviceRecords.values()) {
215
+ if (!record.isStale && record.activeSession) {
216
+ sessions.push(record.activeSession);
217
+ }
218
+ }
219
+
220
+ return sessions;
221
+ }
222
+
223
+ /**
224
+ * Return *all* sessions — active or inactive — that we have stored for this
225
+ * user. This is required for `SessionManager.onEvent` so that it can attach
226
+ * listeners to existing sessions.
227
+ */
228
+ public getAllSessions(): Session[] {
229
+ const sessions: Session[] = [...this.extraSessions];
230
+
231
+ for (const record of this.deviceRecords.values()) {
232
+ if (record.activeSession) {
233
+ sessions.push(record.activeSession);
234
+ }
235
+ sessions.push(...record.inactiveSessions);
236
+ }
237
+
238
+ return sessions;
239
+ }
240
+ }
package/src/types.ts CHANGED
@@ -65,10 +65,10 @@ export type Unsubscribe = () => void;
65
65
  /**
66
66
  * Function that subscribes to Nostr events matching a filter and calls onEvent for each event.
67
67
  */
68
- export type NostrSubscribe = (filter: Filter, onEvent: (e: VerifiedEvent) => void) => Unsubscribe;
69
- export type EncryptFunction = (plaintext: string, pubkey: string) => Promise<string>;
70
- export type DecryptFunction = (ciphertext: string, pubkey: string) => Promise<string>;
71
- export type NostrPublish = (event: UnsignedEvent) => Promise<VerifiedEvent>;
68
+ export type NostrSubscribe = (_filter: Filter, _onEvent: (_e: VerifiedEvent) => void) => Unsubscribe;
69
+ export type EncryptFunction = (_plaintext: string, _pubkey: string) => Promise<string>;
70
+ export type DecryptFunction = (_ciphertext: string, _pubkey: string) => Promise<string>;
71
+ export type NostrPublish = (_event: UnsignedEvent) => Promise<VerifiedEvent>;
72
72
 
73
73
  export type Rumor = UnsignedEvent & { id: string }
74
74
 
@@ -76,7 +76,7 @@ export type Rumor = UnsignedEvent & { id: string }
76
76
  * Callback function for handling decrypted messages
77
77
  * @param message - The decrypted message object
78
78
  */
79
- export type EventCallback = (event: Rumor, outerEvent: VerifiedEvent) => void;
79
+ export type EventCallback = (_event: Rumor, _outerEvent: VerifiedEvent) => void;
80
80
 
81
81
  /**
82
82
  * Message event kind
package/src/utils.ts CHANGED
@@ -51,10 +51,10 @@ export function deserializeSessionState(data: string): SessionState {
51
51
 
52
52
  // Migrate old skipped keys format to new structure
53
53
  if (state.skippedMessageKeys) {
54
- Object.entries(state.skippedMessageKeys).forEach(([pubKey, messageKeys]: [string, any]) => {
54
+ Object.entries(state.skippedMessageKeys).forEach(([pubKey, messageKeys]: [string, unknown]) => {
55
55
  skippedKeys[pubKey] = {
56
56
  headerKeys: state.skippedHeaderKeys?.[pubKey] || [],
57
- messageKeys: messageKeys
57
+ messageKeys: messageKeys as { [msgIndex: number]: Uint8Array }
58
58
  };
59
59
  });
60
60
  }
@@ -102,9 +102,9 @@ export function deserializeSessionState(data: string): SessionState {
102
102
  Object.entries(state.skippedKeys || {}).map(([pubKey, value]) => [
103
103
  pubKey,
104
104
  {
105
- headerKeys: (value as any).headerKeys.map((hex: string) => hexToBytes(hex)),
105
+ headerKeys: (value as { headerKeys: string[] }).headerKeys.map((hex: string) => hexToBytes(hex)),
106
106
  messageKeys: Object.fromEntries(
107
- Object.entries((value as any).messageKeys).map(([msgIndex, hex]) => [
107
+ Object.entries((value as { messageKeys: Record<string, string> }).messageKeys).map(([msgIndex, hex]) => [
108
108
  msgIndex,
109
109
  hexToBytes(hex as string)
110
110
  ])
@@ -117,14 +117,14 @@ export function deserializeSessionState(data: string): SessionState {
117
117
 
118
118
  export async function* createEventStream(session: Session): AsyncGenerator<Rumor, void, unknown> {
119
119
  const messageQueue: Rumor[] = [];
120
- let resolveNext: ((value: Rumor) => void) | null = null;
120
+ let resolveNext: ((_value: Rumor) => void) | null = null;
121
121
 
122
- const unsubscribe = session.onEvent((event) => {
122
+ const unsubscribe = session.onEvent((_event) => {
123
123
  if (resolveNext) {
124
- resolveNext(event);
124
+ resolveNext(_event);
125
125
  resolveNext = null;
126
126
  } else {
127
- messageQueue.push(event);
127
+ messageQueue.push(_event);
128
128
  }
129
129
  });
130
130
 
@@ -153,14 +153,14 @@ export function kdf(input1: Uint8Array, input2: Uint8Array = new Uint8Array(32),
153
153
  return outputs;
154
154
  }
155
155
 
156
- export function skippedMessageIndexKey(nostrSender: string, number: number): string {
157
- return `${nostrSender}:${number}`;
156
+ export function skippedMessageIndexKey(_nostrSender: string, _number: number): string {
157
+ return `${_nostrSender}:${_number}`;
158
158
  }
159
159
 
160
160
  export function getMillisecondTimestamp(event: Rumor) {
161
- const msTag = event.tags?.find(tag => tag[0] === "ms");
161
+ const msTag = event.tags?.find((tag: string[]) => tag[0] === "ms");
162
162
  if (msTag) {
163
163
  return parseInt(msTag[1]);
164
164
  }
165
165
  return event.created_at * 1000;
166
- }
166
+ }