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.
- package/README.md +1 -1
- package/dist/Invite.d.ts +7 -6
- package/dist/Invite.d.ts.map +1 -1
- package/dist/Session.d.ts +3 -3
- package/dist/Session.d.ts.map +1 -1
- package/dist/SessionManager.d.ts +38 -0
- package/dist/SessionManager.d.ts.map +1 -0
- package/dist/StorageAdapter.d.ts +18 -0
- package/dist/StorageAdapter.d.ts.map +1 -0
- package/dist/UserRecord.d.ts +21 -3
- package/dist/UserRecord.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/nostr-double-ratchet.es.js +1407 -1281
- package/dist/nostr-double-ratchet.umd.js +1 -1
- package/dist/types.d.ts +7 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/package.json +19 -17
- package/src/Invite.ts +32 -27
- package/src/Session.ts +11 -23
- package/src/SessionManager.ts +328 -0
- package/src/StorageAdapter.ts +43 -0
- package/src/UserRecord.ts +75 -18
- package/src/index.ts +3 -2
- package/src/types.ts +7 -6
- package/src/utils.ts +12 -12
|
@@ -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
|
|
23
|
-
private
|
|
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
|
-
|
|
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(([
|
|
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.
|
|
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
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 = (
|
|
69
|
-
export type EncryptFunction = (
|
|
70
|
-
export type DecryptFunction = (
|
|
71
|
-
export type NostrPublish = (
|
|
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
|
|
77
|
+
* @param _event - The decrypted message object (Rumor)
|
|
78
|
+
* @param _outerEvent - The outer Nostr event (VerifiedEvent)
|
|
78
79
|
*/
|
|
79
|
-
export type EventCallback = (
|
|
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,
|
|
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
|
|
105
|
+
headerKeys: (value as { headerKeys: string[] }).headerKeys.map((hex: string) => hexToBytes(hex)),
|
|
106
106
|
messageKeys: Object.fromEntries(
|
|
107
|
-
Object.entries((value as
|
|
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: ((
|
|
120
|
+
let resolveNext: ((_value: Rumor) => void) | null = null;
|
|
121
121
|
|
|
122
|
-
const unsubscribe = session.onEvent((
|
|
122
|
+
const unsubscribe = session.onEvent((_event) => {
|
|
123
123
|
if (resolveNext) {
|
|
124
|
-
resolveNext(
|
|
124
|
+
resolveNext(_event);
|
|
125
125
|
resolveNext = null;
|
|
126
126
|
} else {
|
|
127
|
-
messageQueue.push(
|
|
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(
|
|
157
|
-
return `${
|
|
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
|
+
}
|