nostr-double-ratchet 0.0.28 → 0.0.29

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/src/Invite.ts CHANGED
@@ -25,13 +25,13 @@ export class Invite {
25
25
  public sharedSecret: string,
26
26
  public inviter: string,
27
27
  public inviterEphemeralPrivateKey?: Uint8Array,
28
- public label?: string,
28
+ public deviceId?: 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
+ static createNew(inviter: string, deviceId?: string, maxUses?: number): Invite {
35
35
  if (!inviter) {
36
36
  throw new Error("Inviter public key is required");
37
37
  }
@@ -43,7 +43,7 @@ export class Invite {
43
43
  sharedSecret,
44
44
  inviter,
45
45
  inviterEphemeralPrivateKey,
46
- label,
46
+ deviceId,
47
47
  maxUses
48
48
  );
49
49
  }
@@ -82,7 +82,7 @@ export class Invite {
82
82
  data.sharedSecret,
83
83
  data.inviter,
84
84
  data.inviterEphemeralPrivateKey ? new Uint8Array(data.inviterEphemeralPrivateKey) : undefined,
85
- data.label,
85
+ data.deviceId,
86
86
  data.maxUses,
87
87
  data.usedBy
88
88
  );
@@ -105,6 +105,10 @@ export class Invite {
105
105
  const sharedSecret = tags.find(([key]) => key === 'sharedSecret')?.[1];
106
106
  const inviter = event.pubkey;
107
107
 
108
+ // Extract deviceId from the "d" tag (format: double-ratchet/invites/<deviceId>)
109
+ const deviceTag = tags.find(([key]) => key === 'd')?.[1]
110
+ const deviceId = deviceTag?.split('/')?.[2]
111
+
108
112
  if (!inviterEphemeralPublicKey || !sharedSecret) {
109
113
  throw new Error("Invalid invite event: missing session key or sharedSecret");
110
114
  }
@@ -112,7 +116,9 @@ export class Invite {
112
116
  return new Invite(
113
117
  inviterEphemeralPublicKey,
114
118
  sharedSecret,
115
- inviter
119
+ inviter,
120
+ undefined, // inviterEphemeralPrivateKey not available when parsing from event
121
+ deviceId
116
122
  );
117
123
  }
118
124
 
@@ -122,12 +128,10 @@ export class Invite {
122
128
  authors: [user],
123
129
  "#l": ["double-ratchet/invites"]
124
130
  };
125
- let latest = 0;
131
+ const seenIds = new Set<string>()
126
132
  const unsub = subscribe(filter, (event) => {
127
- if (!event.created_at || event.created_at <= latest) {
128
- return;
129
- }
130
- latest = event.created_at;
133
+ if (seenIds.has(event.id)) return
134
+ seenIds.add(event.id)
131
135
  try {
132
136
  const inviteLink = Invite.fromEvent(event);
133
137
  onInvite(inviteLink);
@@ -147,7 +151,7 @@ export class Invite {
147
151
  sharedSecret: this.sharedSecret,
148
152
  inviter: this.inviter,
149
153
  inviterEphemeralPrivateKey: this.inviterEphemeralPrivateKey ? Array.from(this.inviterEphemeralPrivateKey) : undefined,
150
- label: this.label,
154
+ deviceId: this.deviceId,
151
155
  maxUses: this.maxUses,
152
156
  usedBy: this.usedBy,
153
157
  });
@@ -167,9 +171,9 @@ export class Invite {
167
171
  return url.toString();
168
172
  }
169
173
 
170
- getEvent(deviceName: string): UnsignedEvent {
171
- if (!deviceName) {
172
- throw new Error("Device name is required");
174
+ getEvent(): UnsignedEvent {
175
+ if (!this.deviceId) {
176
+ throw new Error("Device ID is required");
173
177
  }
174
178
  return {
175
179
  kind: INVITE_EVENT_KIND,
@@ -179,7 +183,7 @@ export class Invite {
179
183
  tags: [
180
184
  ['ephemeralKey', this.inviterEphemeralPublicKey],
181
185
  ['sharedSecret', this.sharedSecret],
182
- ['d', 'double-ratchet/invites/' + deviceName],
186
+ ['d', 'double-ratchet/invites/' + this.deviceId],
183
187
  ['l', 'double-ratchet/invites']
184
188
  ],
185
189
  };
@@ -187,11 +191,12 @@ export class Invite {
187
191
 
188
192
  /**
189
193
  * Called by the invitee. Accepts the invite and creates a new session with the inviter.
190
- *
191
- * @param inviteeSecretKey - The invitee's secret key or a signing function
194
+ *
192
195
  * @param nostrSubscribe - A function to subscribe to Nostr events
196
+ * @param inviteePublicKey - The invitee's public key
197
+ * @param encryptor - The invitee's secret key or a signing/encrypt function
193
198
  * @returns An object containing the new session and an event to be published
194
- *
199
+ *
195
200
  * 1. Inner event: No signature, content encrypted with DH(inviter, invitee).
196
201
  * Purpose: Authenticate invitee. Contains invitee session key.
197
202
  * 2. Envelope: No signature, content encrypted with DH(inviter, random key).
package/src/Session.ts CHANGED
@@ -38,9 +38,9 @@ export class Session {
38
38
 
39
39
  /**
40
40
  * Initializes a new secure communication session
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
- * @param theirNextNostrPublicKey The public key of the other party
43
- * @param ourCurrentPrivateKey Our current private key for Nostr
41
+ * @param nostrSubscribe Function to subscribe to Nostr events. Make sure it deduplicates events (doesn't return the same event twice), otherwise you'll see decryption errors!
42
+ * @param theirEphemeralNostrPublicKey The ephemeral public key of the other party for the initial handshake
43
+ * @param ourEphemeralNostrPrivateKey Our ephemeral private key for the initial handshake
44
44
  * @param isInitiator Whether we are initiating the conversation (true) or responding (false)
45
45
  * @param sharedSecret Initial shared secret for securing the first message chain
46
46
  * @param name Optional name for the session (for debugging)
@@ -2,6 +2,9 @@ import { CHAT_MESSAGE_KIND, NostrPublish, NostrSubscribe, Rumor, Unsubscribe } f
2
2
  import { UserRecord } from "./UserRecord"
3
3
  import { Invite } from "./Invite"
4
4
  import { getPublicKey } from "nostr-tools"
5
+ import { StorageAdapter, InMemoryStorageAdapter } from "./StorageAdapter"
6
+ import { serializeSessionState, deserializeSessionState } from "./utils"
7
+ import { Session } from "./Session"
5
8
 
6
9
  export default class SessionManager {
7
10
  private userRecords: Map<string, UserRecord> = new Map()
@@ -9,13 +12,176 @@ export default class SessionManager {
9
12
  private nostrPublish: NostrPublish
10
13
  private ourIdentityKey: Uint8Array
11
14
  private inviteUnsubscribes: Map<string, Unsubscribe> = new Map()
12
- private ownDeviceInvites: Map<string, Invite | null> = new Map()
15
+ private deviceId: string
16
+ private invite?: Invite
17
+ private storage: StorageAdapter
13
18
 
14
- constructor(ourIdentityKey: Uint8Array, nostrSubscribe: NostrSubscribe, nostrPublish: NostrPublish) {
19
+ constructor(
20
+ ourIdentityKey: Uint8Array,
21
+ deviceId: string,
22
+ nostrSubscribe: NostrSubscribe,
23
+ nostrPublish: NostrPublish,
24
+ storage: StorageAdapter = new InMemoryStorageAdapter(),
25
+ ) {
15
26
  this.userRecords = new Map()
16
27
  this.nostrSubscribe = nostrSubscribe
17
28
  this.nostrPublish = nostrPublish
18
29
  this.ourIdentityKey = ourIdentityKey
30
+ this.deviceId = deviceId
31
+ this.storage = storage
32
+
33
+ // Kick off initialisation in background for backwards compatibility
34
+ // Users that need to wait can call await manager.init()
35
+ this.init()
36
+ }
37
+
38
+ private _initialised = false
39
+
40
+ /**
41
+ * Perform asynchronous initialisation steps: create (or load) our invite,
42
+ * publish it, hydrate sessions from storage and subscribe to new invites.
43
+ * Can be awaited by callers that need deterministic readiness.
44
+ */
45
+ public async init(): Promise<void> {
46
+ if (this._initialised) return
47
+
48
+ const ourPublicKey = getPublicKey(this.ourIdentityKey)
49
+
50
+ // 1. Hydrate existing sessions (placeholder for future implementation)
51
+ await this.loadSessions()
52
+
53
+ // 2. Create or load our own invite
54
+ let invite: Invite | undefined
55
+ try {
56
+ const stored = await this.storage.get<string>(`invite/${this.deviceId}`)
57
+ if (stored) {
58
+ invite = Invite.deserialize(stored)
59
+ }
60
+ } catch {/* ignore malformed */}
61
+
62
+ if (!invite) {
63
+ invite = Invite.createNew(ourPublicKey, this.deviceId)
64
+ await this.storage.put(`invite/${this.deviceId}`, invite.serialize()).catch(() => {})
65
+ }
66
+ this.invite = invite
67
+
68
+ // 2b. Listen for acceptances of *our* invite and create sessions
69
+ this.invite.listen(
70
+ this.ourIdentityKey,
71
+ this.nostrSubscribe,
72
+ (session, inviteePubkey) => {
73
+ if (!inviteePubkey) return
74
+ try {
75
+ let userRecord = this.userRecords.get(inviteePubkey)
76
+ if (!userRecord) {
77
+ userRecord = new UserRecord(inviteePubkey, this.nostrSubscribe)
78
+ this.userRecords.set(inviteePubkey, userRecord)
79
+ }
80
+
81
+ const deviceKey = session.name || 'unknown'
82
+ userRecord.upsertSession(deviceKey, session)
83
+ this.saveSession(inviteePubkey, deviceKey, session)
84
+
85
+ session.onEvent((_event: Rumor) => {
86
+ this.internalSubscriptions.forEach(cb => cb(_event))
87
+ })
88
+ } catch {/* ignore errors */}
89
+ }
90
+ )
91
+
92
+ // 3. Subscribe to our own invites from other devices
93
+ Invite.fromUser(ourPublicKey, this.nostrSubscribe, async (invite) => {
94
+ try {
95
+ const inviteDeviceId = invite['deviceId'] || 'unknown'
96
+ if (!inviteDeviceId || inviteDeviceId === this.deviceId) {
97
+ return
98
+ }
99
+
100
+ const existingRecord = this.userRecords.get(ourPublicKey)
101
+ if (existingRecord?.getActiveSessions().some(session => session.name === inviteDeviceId)) {
102
+ return
103
+ }
104
+
105
+ const { session, event } = await invite.accept(
106
+ this.nostrSubscribe,
107
+ ourPublicKey,
108
+ this.ourIdentityKey
109
+ )
110
+ this.nostrPublish(event)?.catch(() => {})
111
+
112
+ this.saveSession(ourPublicKey, inviteDeviceId, session)
113
+
114
+ let userRecord = this.userRecords.get(ourPublicKey)
115
+ if (!userRecord) {
116
+ userRecord = new UserRecord(ourPublicKey, this.nostrSubscribe)
117
+ this.userRecords.set(ourPublicKey, userRecord)
118
+ }
119
+ const deviceId = invite['deviceId'] || event.id || 'unknown'
120
+ userRecord.upsertSession(deviceId, session)
121
+ this.saveSession(ourPublicKey, deviceId, session)
122
+
123
+ session.onEvent((_event: Rumor) => {
124
+ this.internalSubscriptions.forEach(cb => cb(_event))
125
+ })
126
+ } catch (err) {
127
+ // eslint-disable-next-line no-console
128
+ console.error('Own-invite accept failed', err)
129
+ }
130
+ })
131
+
132
+ this._initialised = true
133
+ await this.nostrPublish(this.invite.getEvent()).catch(() => {})
134
+ }
135
+
136
+ private async loadSessions() {
137
+ const base = 'session/'
138
+ const keys = await this.storage.list(base)
139
+ for (const key of keys) {
140
+ const rest = key.substring(base.length)
141
+ const idx = rest.indexOf('/')
142
+ if (idx === -1) continue
143
+ const ownerPubKey = rest.substring(0, idx)
144
+ const deviceId = rest.substring(idx + 1) || 'unknown'
145
+
146
+ const data = await this.storage.get<string>(key)
147
+ if (!data) continue
148
+ try {
149
+ const state = deserializeSessionState(data)
150
+ const session = new Session(this.nostrSubscribe, state)
151
+
152
+ let userRecord = this.userRecords.get(ownerPubKey)
153
+ if (!userRecord) {
154
+ userRecord = new UserRecord(ownerPubKey, this.nostrSubscribe)
155
+ this.userRecords.set(ownerPubKey, userRecord)
156
+ }
157
+ userRecord.upsertSession(deviceId, session)
158
+ this.saveSession(ownerPubKey, deviceId, session)
159
+
160
+ session.onEvent((_event: Rumor) => {
161
+ this.internalSubscriptions.forEach(cb => cb(_event))
162
+ })
163
+ } catch {
164
+ // corrupted entry — ignore
165
+ }
166
+ }
167
+ }
168
+
169
+ private async saveSession(ownerPubKey: string, deviceId: string, session: Session) {
170
+ try {
171
+ const key = `session/${ownerPubKey}/${deviceId}`
172
+ await this.storage.put(key, serializeSessionState(session.state))
173
+ } catch {/* ignore */}
174
+ }
175
+
176
+ getDeviceId(): string {
177
+ return this.deviceId
178
+ }
179
+
180
+ getInvite(): Invite {
181
+ if (!this.invite) {
182
+ throw new Error("SessionManager not initialised yet")
183
+ }
184
+ return this.invite
19
185
  }
20
186
 
21
187
  async sendText(recipientIdentityKey: string, text: string) {
@@ -32,9 +198,10 @@ export default class SessionManager {
32
198
  // Send to recipient's devices
33
199
  const userRecord = this.userRecords.get(recipientIdentityKey)
34
200
  if (!userRecord) {
35
- // Listen for invites from recipient
201
+ // Listen for invites from recipient and return without throwing; caller
202
+ // can await a subsequent session establishment.
36
203
  this.listenToUser(recipientIdentityKey)
37
- throw new Error("No active session with user. Listening for invites.")
204
+ return []
38
205
  }
39
206
 
40
207
  // Send to all active sessions with recipient
@@ -67,7 +234,7 @@ export default class SessionManager {
67
234
  getPublicKey(this.ourIdentityKey),
68
235
  this.ourIdentityKey
69
236
  )
70
- this.nostrPublish(event)
237
+ this.nostrPublish(event)?.catch(() => {})
71
238
 
72
239
  // Store the new session
73
240
  let userRecord = this.userRecords.get(userPubkey)
@@ -75,10 +242,11 @@ export default class SessionManager {
75
242
  userRecord = new UserRecord(userPubkey, this.nostrSubscribe)
76
243
  this.userRecords.set(userPubkey, userRecord)
77
244
  }
78
- userRecord.insertSession(event.id || 'unknown', session)
245
+ const deviceId = (_invite instanceof Invite && _invite.deviceId) ? _invite.deviceId : event.id || 'unknown'
246
+ this.saveSession(userPubkey, deviceId, session)
79
247
 
80
248
  // Set up event handling for the new session
81
- session.onEvent((_event) => {
249
+ session.onEvent((_event: Rumor) => {
82
250
  this.internalSubscriptions.forEach(callback => callback(_event))
83
251
  })
84
252
 
@@ -100,9 +268,9 @@ export default class SessionManager {
100
268
  }
101
269
 
102
270
  // Update onEvent to include internalSubscriptions management
103
- private internalSubscriptions: Set<(_event: Rumor) => void> = new Set()
271
+ private internalSubscriptions: Set<(event: Rumor) => void> = new Set()
104
272
 
105
- onEvent(callback: (_event: Rumor) => void) {
273
+ onEvent(callback: (event: Rumor) => void) {
106
274
  this.internalSubscriptions.add(callback)
107
275
 
108
276
  // Subscribe to existing sessions
@@ -135,31 +303,26 @@ export default class SessionManager {
135
303
  }
136
304
  this.userRecords.clear()
137
305
  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
306
  }
151
307
 
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
- }
308
+ /**
309
+ * Accept an invite as our own device, persist the session, and publish the acceptance event.
310
+ * Used for multi-device flows where a user adds a new device.
311
+ */
312
+ public async acceptOwnInvite(invite: Invite) {
313
+ const ourPublicKey = getPublicKey(this.ourIdentityKey);
314
+ const { session, event } = await invite.accept(
315
+ this.nostrSubscribe,
316
+ ourPublicKey,
317
+ this.ourIdentityKey
318
+ );
319
+ let userRecord = this.userRecords.get(ourPublicKey);
320
+ if (!userRecord) {
321
+ userRecord = new UserRecord(ourPublicKey, this.nostrSubscribe);
322
+ this.userRecords.set(ourPublicKey, userRecord);
162
323
  }
163
- return active
324
+ userRecord.upsertSession(session.name || 'unknown', session);
325
+ await this.saveSession(ourPublicKey, session.name || 'unknown', session);
326
+ this.nostrPublish(event)?.catch(() => {});
164
327
  }
165
328
  }
@@ -0,0 +1,43 @@
1
+ /*
2
+ * Simple async key value storage interface plus an in memory implementation.
3
+ *
4
+ * All methods are Promise based to accommodate back ends like
5
+ * IndexedDB, SQLite, remote HTTP APIs, etc. For environments where you only
6
+ * need ephemeral data (tests, Node scripts) the InMemoryStorageAdapter can be
7
+ * used directly.
8
+ */
9
+
10
+ export interface StorageAdapter {
11
+ /** Retrieve a value by key. */
12
+ get<T = unknown>(key: string): Promise<T | undefined>
13
+ /** Store a value by key. */
14
+ put<T = unknown>(key: string, value: T): Promise<void>
15
+ /** Delete a stored value by key. */
16
+ del(key: string): Promise<void>
17
+ /** List all keys that start with the given prefix. */
18
+ list(prefix?: string): Promise<string[]>
19
+ }
20
+
21
+ export class InMemoryStorageAdapter implements StorageAdapter {
22
+ private store = new Map<string, unknown>()
23
+
24
+ async get<T = unknown>(key: string): Promise<T | undefined> {
25
+ return this.store.get(key) as T | undefined
26
+ }
27
+
28
+ async put<T = unknown>(key: string, value: T): Promise<void> {
29
+ this.store.set(key, value)
30
+ }
31
+
32
+ async del(key: string): Promise<void> {
33
+ this.store.delete(key)
34
+ }
35
+
36
+ async list(prefix = ''): Promise<string[]> {
37
+ const keys: string[] = []
38
+ for (const k of this.store.keys()) {
39
+ if (k.startsWith(prefix)) keys.push(k)
40
+ }
41
+ return keys
42
+ }
43
+ }
package/src/UserRecord.ts CHANGED
@@ -17,15 +17,6 @@ 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[] = [];
29
20
 
30
21
  constructor(
31
22
  public _userId: string,
@@ -53,18 +44,7 @@ export class UserRecord {
53
44
  * Inserts a new session for a device, making it the active session
54
45
  */
55
46
  public insertSession(deviceId: string, session: Session): void {
56
- const record = this.deviceRecords.get(deviceId);
57
- if (!record) {
58
- throw new Error(`No device record found for ${deviceId}`);
59
- }
60
-
61
- // Move current active session to inactive list if it exists
62
- if (record.activeSession) {
63
- record.inactiveSessions.unshift(record.activeSession);
64
- }
65
-
66
- // Set new session as active
67
- record.activeSession = session;
47
+ this.upsertSession(deviceId, session)
68
48
  }
69
49
 
70
50
  /**
@@ -194,22 +174,13 @@ export class UserRecord {
194
174
  // Helper methods used by SessionManager (WIP):
195
175
  // ---------------------------------------------------------------------------
196
176
 
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
177
  /**
207
178
  * Return all sessions that are currently considered *active*.
208
179
  * For now this means any session in a non-stale device record as well as
209
180
  * all sessions added through `addSession`.
210
181
  */
211
182
  public getActiveSessions(): Session[] {
212
- const sessions: Session[] = [...this.extraSessions];
183
+ const sessions: Session[] = [];
213
184
 
214
185
  for (const record of this.deviceRecords.values()) {
215
186
  if (!record.isStale && record.activeSession) {
@@ -226,7 +197,7 @@ export class UserRecord {
226
197
  * listeners to existing sessions.
227
198
  */
228
199
  public getAllSessions(): Session[] {
229
- const sessions: Session[] = [...this.extraSessions];
200
+ const sessions: Session[] = [];
230
201
 
231
202
  for (const record of this.deviceRecords.values()) {
232
203
  if (record.activeSession) {
@@ -237,4 +208,32 @@ export class UserRecord {
237
208
 
238
209
  return sessions;
239
210
  }
211
+
212
+ /**
213
+ * Unified helper that either associates the session with a device record
214
+ * (if deviceId provided **and** the record exists) or falls back to the
215
+ * legacy extraSessions list.
216
+ */
217
+ public upsertSession(deviceId: string | undefined, session: Session) {
218
+ if (!deviceId) {
219
+ deviceId = 'unknown'
220
+ }
221
+
222
+ let record = this.deviceRecords.get(deviceId)
223
+ if (!record) {
224
+ record = {
225
+ publicKey: session.state?.theirNextNostrPublicKey || '',
226
+ inactiveSessions: [],
227
+ isStale: false
228
+ }
229
+ this.deviceRecords.set(deviceId, record)
230
+ }
231
+
232
+ if (record.activeSession) {
233
+ record.inactiveSessions.unshift(record.activeSession)
234
+ }
235
+ // Ensure session name matches deviceId for easier identification
236
+ session.name = deviceId
237
+ record.activeSession = session
238
+ }
240
239
  }
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
- export * from "./Session.ts"
1
+ export * from "./Session"
2
2
  export * from "./Invite"
3
3
  export * from "./types"
4
- export * from "./utils"
4
+ export * from "./utils"
5
+ export * from "./SessionManager"
package/src/types.ts CHANGED
@@ -74,7 +74,8 @@ export type Rumor = UnsignedEvent & { id: string }
74
74
 
75
75
  /**
76
76
  * Callback function for handling decrypted messages
77
- * @param message - The decrypted message object
77
+ * @param _event - The decrypted message object (Rumor)
78
+ * @param _outerEvent - The outer Nostr event (VerifiedEvent)
78
79
  */
79
80
  export type EventCallback = (_event: Rumor, _outerEvent: VerifiedEvent) => void;
80
81