nostr-double-ratchet 0.0.27 → 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.
@@ -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
@@ -19,9 +19,10 @@ export class UserRecord {
19
19
  private staleTimestamp?: number;
20
20
 
21
21
  constructor(
22
- public userId: string,
23
- private nostrSubscribe: NostrSubscribe,
24
- ) {}
22
+ public _userId: string,
23
+ private _nostrSubscribe: NostrSubscribe,
24
+ ) {
25
+ }
25
26
 
26
27
  /**
27
28
  * Adds or updates a device record for this user
@@ -43,18 +44,7 @@ export class UserRecord {
43
44
  * Inserts a new session for a device, making it the active session
44
45
  */
45
46
  public insertSession(deviceId: string, session: Session): void {
46
- const record = this.deviceRecords.get(deviceId);
47
- if (!record) {
48
- throw new Error(`No device record found for ${deviceId}`);
49
- }
50
-
51
- // Move current active session to inactive list if it exists
52
- if (record.activeSession) {
53
- record.inactiveSessions.unshift(record.activeSession);
54
- }
55
-
56
- // Set new session as active
57
- record.activeSession = session;
47
+ this.upsertSession(deviceId, session)
58
48
  }
59
49
 
60
50
  /**
@@ -109,7 +99,7 @@ export class UserRecord {
109
99
  if (this.isStale) return [];
110
100
 
111
101
  return Array.from(this.deviceRecords.entries())
112
- .filter(([_, record]) => !record.isStale && record.activeSession)
102
+ .filter(([, record]) => !record.isStale && record.activeSession)
113
103
  .map(([deviceId, record]) => [deviceId, record.activeSession!]);
114
104
  }
115
105
 
@@ -129,7 +119,7 @@ export class UserRecord {
129
119
  }
130
120
 
131
121
  const session = Session.init(
132
- this.nostrSubscribe,
122
+ this._nostrSubscribe,
133
123
  record.publicKey,
134
124
  ourCurrentPrivateKey,
135
125
  isInitiator,
@@ -179,4 +169,71 @@ export class UserRecord {
179
169
  });
180
170
  this.deviceRecords.clear();
181
171
  }
182
- }
172
+
173
+ // ---------------------------------------------------------------------------
174
+ // Helper methods used by SessionManager (WIP):
175
+ // ---------------------------------------------------------------------------
176
+
177
+ /**
178
+ * Return all sessions that are currently considered *active*.
179
+ * For now this means any session in a non-stale device record as well as
180
+ * all sessions added through `addSession`.
181
+ */
182
+ public getActiveSessions(): Session[] {
183
+ const sessions: Session[] = [];
184
+
185
+ for (const record of this.deviceRecords.values()) {
186
+ if (!record.isStale && record.activeSession) {
187
+ sessions.push(record.activeSession);
188
+ }
189
+ }
190
+
191
+ return sessions;
192
+ }
193
+
194
+ /**
195
+ * Return *all* sessions — active or inactive — that we have stored for this
196
+ * user. This is required for `SessionManager.onEvent` so that it can attach
197
+ * listeners to existing sessions.
198
+ */
199
+ public getAllSessions(): Session[] {
200
+ const sessions: Session[] = [];
201
+
202
+ for (const record of this.deviceRecords.values()) {
203
+ if (record.activeSession) {
204
+ sessions.push(record.activeSession);
205
+ }
206
+ sessions.push(...record.inactiveSessions);
207
+ }
208
+
209
+ return sessions;
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
+ }
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
@@ -65,18 +65,19 @@ 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
 
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
- export type EventCallback = (event: Rumor, outerEvent: VerifiedEvent) => void;
80
+ export type EventCallback = (_event: Rumor, _outerEvent: VerifiedEvent) => void;
80
81
 
81
82
  /**
82
83
  * 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
+ }