nostr-double-ratchet 0.0.32 → 0.0.34
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 +4 -2
- package/dist/Invite.d.ts +9 -3
- package/dist/Invite.d.ts.map +1 -1
- package/dist/UserRecord.d.ts +78 -31
- package/dist/UserRecord.d.ts.map +1 -1
- package/dist/nostr-double-ratchet.es.js +563 -535
- package/dist/nostr-double-ratchet.umd.js +1 -1
- package/package.json +4 -4
- package/src/Invite.ts +47 -5
- package/src/UserRecord.ts +235 -146
package/src/Invite.ts
CHANGED
|
@@ -130,6 +130,10 @@ export class Invite {
|
|
|
130
130
|
};
|
|
131
131
|
const seenIds = new Set<string>()
|
|
132
132
|
const unsub = subscribe(filter, (event) => {
|
|
133
|
+
if (event.pubkey !== user) {
|
|
134
|
+
console.error("Got invite event from wrong user", event.pubkey, "expected", user)
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
133
137
|
if (seenIds.has(event.id)) return
|
|
134
138
|
seenIds.add(event.id)
|
|
135
139
|
try {
|
|
@@ -189,16 +193,38 @@ export class Invite {
|
|
|
189
193
|
};
|
|
190
194
|
}
|
|
191
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Creates an "invite tombstone" event that clears the original content and removes the list tag.
|
|
198
|
+
* Used when the inviter logs out and wants to make the invite invisible to other devices.
|
|
199
|
+
*/
|
|
200
|
+
getDeletionEvent(): UnsignedEvent {
|
|
201
|
+
if (!this.deviceId) {
|
|
202
|
+
throw new Error("Device ID is required");
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
kind: INVITE_EVENT_KIND,
|
|
206
|
+
pubkey: this.inviter,
|
|
207
|
+
content: "", // deliberately empty
|
|
208
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
209
|
+
tags: [
|
|
210
|
+
['ephemeralKey', this.inviterEphemeralPublicKey],
|
|
211
|
+
['sharedSecret', this.sharedSecret],
|
|
212
|
+
['d', 'double-ratchet/invites/' + this.deviceId], // same d tag
|
|
213
|
+
],
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
192
217
|
/**
|
|
193
218
|
* Called by the invitee. Accepts the invite and creates a new session with the inviter.
|
|
194
219
|
*
|
|
195
220
|
* @param nostrSubscribe - A function to subscribe to Nostr events
|
|
196
221
|
* @param inviteePublicKey - The invitee's public key
|
|
197
222
|
* @param encryptor - The invitee's secret key or a signing/encrypt function
|
|
223
|
+
* @param deviceId - Optional device ID to identify the invitee's device
|
|
198
224
|
* @returns An object containing the new session and an event to be published
|
|
199
225
|
*
|
|
200
226
|
* 1. Inner event: No signature, content encrypted with DH(inviter, invitee).
|
|
201
|
-
* Purpose: Authenticate invitee. Contains invitee session key.
|
|
227
|
+
* Purpose: Authenticate invitee. Contains invitee session key and deviceId.
|
|
202
228
|
* 2. Envelope: No signature, content encrypted with DH(inviter, random key).
|
|
203
229
|
* Purpose: Contains inner event. Hides invitee from others who might have the shared Nostr key.
|
|
204
230
|
|
|
@@ -209,6 +235,7 @@ export class Invite {
|
|
|
209
235
|
nostrSubscribe: NostrSubscribe,
|
|
210
236
|
inviteePublicKey: string,
|
|
211
237
|
encryptor: Uint8Array | EncryptFunction,
|
|
238
|
+
deviceId?: string,
|
|
212
239
|
): Promise<{ session: Session, event: VerifiedEvent }> {
|
|
213
240
|
const inviteeSessionKey = generateSecretKey();
|
|
214
241
|
const inviteeSessionPublicKey = getPublicKey(inviteeSessionKey);
|
|
@@ -223,7 +250,11 @@ export class Invite {
|
|
|
223
250
|
encryptor :
|
|
224
251
|
(plaintext: string, pubkey: string) => Promise.resolve(nip44.encrypt(plaintext, getConversationKey(encryptor, pubkey)));
|
|
225
252
|
|
|
226
|
-
const
|
|
253
|
+
const payload = JSON.stringify({
|
|
254
|
+
sessionKey: inviteeSessionPublicKey,
|
|
255
|
+
deviceId: deviceId
|
|
256
|
+
});
|
|
257
|
+
const dhEncrypted = await encrypt(payload, inviterPublicKey);
|
|
227
258
|
|
|
228
259
|
const innerEvent = {
|
|
229
260
|
pubkey: inviteePublicKey,
|
|
@@ -247,7 +278,7 @@ export class Invite {
|
|
|
247
278
|
return { session, event: finalizeEvent(envelope, randomSenderKey) };
|
|
248
279
|
}
|
|
249
280
|
|
|
250
|
-
listen(decryptor: Uint8Array | DecryptFunction, nostrSubscribe: NostrSubscribe, onSession: (_session: Session, _identity?: string) => void): Unsubscribe {
|
|
281
|
+
listen(decryptor: Uint8Array | DecryptFunction, nostrSubscribe: NostrSubscribe, onSession: (_session: Session, _identity: string, _deviceId?: string) => void): Unsubscribe {
|
|
251
282
|
if (!this.inviterEphemeralPrivateKey) {
|
|
252
283
|
throw new Error("Inviter session key is not available");
|
|
253
284
|
}
|
|
@@ -279,12 +310,23 @@ export class Invite {
|
|
|
279
310
|
decryptor :
|
|
280
311
|
(ciphertext: string, pubkey: string) => Promise.resolve(nip44.decrypt(ciphertext, getConversationKey(decryptor, pubkey)));
|
|
281
312
|
|
|
282
|
-
const
|
|
313
|
+
const decryptedPayload = await innerDecrypt(dhEncrypted, inviteeIdentity);
|
|
314
|
+
|
|
315
|
+
let inviteeSessionPublicKey: string;
|
|
316
|
+
let deviceId: string | undefined;
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
const parsed = JSON.parse(decryptedPayload);
|
|
320
|
+
inviteeSessionPublicKey = parsed.sessionKey;
|
|
321
|
+
deviceId = parsed.deviceId;
|
|
322
|
+
} catch {
|
|
323
|
+
inviteeSessionPublicKey = decryptedPayload;
|
|
324
|
+
}
|
|
283
325
|
|
|
284
326
|
const name = event.id;
|
|
285
327
|
const session = Session.init(nostrSubscribe, inviteeSessionPublicKey, this.inviterEphemeralPrivateKey!, false, sharedSecret, name);
|
|
286
328
|
|
|
287
|
-
onSession(session, inviteeIdentity);
|
|
329
|
+
onSession(session, inviteeIdentity, deviceId);
|
|
288
330
|
} catch {
|
|
289
331
|
}
|
|
290
332
|
});
|
package/src/UserRecord.ts
CHANGED
|
@@ -2,16 +2,18 @@ import { Session } from './Session';
|
|
|
2
2
|
import { NostrSubscribe } from './types';
|
|
3
3
|
|
|
4
4
|
interface DeviceRecord {
|
|
5
|
+
deviceId: string;
|
|
5
6
|
publicKey: string;
|
|
6
7
|
activeSession?: Session;
|
|
7
8
|
inactiveSessions: Session[];
|
|
8
9
|
isStale: boolean;
|
|
9
10
|
staleTimestamp?: number;
|
|
11
|
+
lastActivity?: number;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
+
* Manages sessions for a single user across multiple devices
|
|
16
|
+
* Structure: UserRecord → DeviceRecord → Sessions
|
|
15
17
|
*/
|
|
16
18
|
export class UserRecord {
|
|
17
19
|
private deviceRecords: Map<string, DeviceRecord> = new Map();
|
|
@@ -19,231 +21,318 @@ export class UserRecord {
|
|
|
19
21
|
private staleTimestamp?: number;
|
|
20
22
|
|
|
21
23
|
constructor(
|
|
22
|
-
public
|
|
23
|
-
private
|
|
24
|
+
public readonly userId: string,
|
|
25
|
+
private readonly nostrSubscribe: NostrSubscribe,
|
|
24
26
|
) {
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Device Management
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
27
33
|
/**
|
|
28
|
-
*
|
|
34
|
+
* Creates or updates a device record for this user
|
|
29
35
|
*/
|
|
30
|
-
public
|
|
31
|
-
|
|
36
|
+
public upsertDevice(deviceId: string, publicKey: string): DeviceRecord {
|
|
37
|
+
let record = this.deviceRecords.get(deviceId);
|
|
32
38
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
if (!record) {
|
|
40
|
+
record = {
|
|
41
|
+
deviceId,
|
|
36
42
|
publicKey,
|
|
37
43
|
inactiveSessions: [],
|
|
38
|
-
isStale: false
|
|
39
|
-
|
|
44
|
+
isStale: false,
|
|
45
|
+
lastActivity: Date.now()
|
|
46
|
+
};
|
|
47
|
+
this.deviceRecords.set(deviceId, record);
|
|
48
|
+
} else if (record.publicKey !== publicKey) {
|
|
49
|
+
// Public key changed - mark old sessions as inactive and update
|
|
50
|
+
if (record.activeSession) {
|
|
51
|
+
record.inactiveSessions.push(record.activeSession);
|
|
52
|
+
record.activeSession = undefined;
|
|
53
|
+
}
|
|
54
|
+
record.publicKey = publicKey;
|
|
55
|
+
record.lastActivity = Date.now();
|
|
40
56
|
}
|
|
57
|
+
|
|
58
|
+
return record;
|
|
41
59
|
}
|
|
42
60
|
|
|
43
61
|
/**
|
|
44
|
-
*
|
|
62
|
+
* Gets a device record by deviceId
|
|
45
63
|
*/
|
|
46
|
-
public
|
|
47
|
-
this.
|
|
64
|
+
public getDevice(deviceId: string): DeviceRecord | undefined {
|
|
65
|
+
return this.deviceRecords.get(deviceId);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Gets all device records for this user
|
|
70
|
+
*/
|
|
71
|
+
public getAllDevices(): DeviceRecord[] {
|
|
72
|
+
return Array.from(this.deviceRecords.values());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Gets all active (non-stale) device records
|
|
77
|
+
*/
|
|
78
|
+
public getActiveDevices(): DeviceRecord[] {
|
|
79
|
+
if (this.isStale) return [];
|
|
80
|
+
return Array.from(this.deviceRecords.values()).filter(device => !device.isStale);
|
|
48
81
|
}
|
|
49
82
|
|
|
50
83
|
/**
|
|
51
|
-
*
|
|
84
|
+
* Removes a device record and closes all its sessions
|
|
52
85
|
*/
|
|
53
|
-
public
|
|
86
|
+
public removeDevice(deviceId: string): boolean {
|
|
54
87
|
const record = this.deviceRecords.get(deviceId);
|
|
55
|
-
if (!record)
|
|
56
|
-
throw new Error(`No device record found for ${deviceId}`);
|
|
57
|
-
}
|
|
88
|
+
if (!record) return false;
|
|
58
89
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
90
|
+
// Close all sessions for this device
|
|
91
|
+
record.activeSession?.close();
|
|
92
|
+
record.inactiveSessions.forEach(session => session.close());
|
|
93
|
+
|
|
94
|
+
return this.deviceRecords.delete(deviceId);
|
|
95
|
+
}
|
|
63
96
|
|
|
64
|
-
|
|
65
|
-
|
|
97
|
+
// ============================================================================
|
|
98
|
+
// Session Management
|
|
99
|
+
// ============================================================================
|
|
66
100
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
101
|
+
/**
|
|
102
|
+
* Adds or updates a session for a specific device
|
|
103
|
+
*/
|
|
104
|
+
public upsertSession(deviceId: string, session: Session, publicKey?: string): void {
|
|
105
|
+
const device = this.upsertDevice(deviceId, publicKey || session.state?.theirNextNostrPublicKey || '');
|
|
106
|
+
|
|
107
|
+
// If there's an active session, move it to inactive
|
|
108
|
+
if (device.activeSession) {
|
|
109
|
+
device.inactiveSessions.unshift(device.activeSession);
|
|
70
110
|
}
|
|
71
111
|
|
|
72
|
-
// Set
|
|
73
|
-
|
|
112
|
+
// Set the new session as active
|
|
113
|
+
session.name = deviceId; // Ensure session name matches deviceId
|
|
114
|
+
device.activeSession = session;
|
|
115
|
+
device.lastActivity = Date.now();
|
|
74
116
|
}
|
|
75
117
|
|
|
76
118
|
/**
|
|
77
|
-
*
|
|
119
|
+
* Gets the active session for a specific device
|
|
78
120
|
*/
|
|
79
|
-
public
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
record.isStale = true;
|
|
83
|
-
record.staleTimestamp = Date.now();
|
|
84
|
-
}
|
|
121
|
+
public getActiveSession(deviceId: string): Session | undefined {
|
|
122
|
+
const device = this.deviceRecords.get(deviceId);
|
|
123
|
+
return device?.isStale ? undefined : device?.activeSession;
|
|
85
124
|
}
|
|
86
125
|
|
|
87
126
|
/**
|
|
88
|
-
*
|
|
127
|
+
* Gets all sessions (active + inactive) for a specific device
|
|
89
128
|
*/
|
|
90
|
-
public
|
|
91
|
-
|
|
92
|
-
|
|
129
|
+
public getDeviceSessions(deviceId: string): Session[] {
|
|
130
|
+
const device = this.deviceRecords.get(deviceId);
|
|
131
|
+
if (!device) return [];
|
|
132
|
+
|
|
133
|
+
const sessions: Session[] = [];
|
|
134
|
+
if (device.activeSession) {
|
|
135
|
+
sessions.push(device.activeSession);
|
|
136
|
+
}
|
|
137
|
+
sessions.push(...device.inactiveSessions);
|
|
138
|
+
return sessions;
|
|
93
139
|
}
|
|
94
140
|
|
|
95
141
|
/**
|
|
96
|
-
* Gets all
|
|
142
|
+
* Gets all active sessions across all devices for this user
|
|
97
143
|
*/
|
|
98
|
-
public
|
|
99
|
-
|
|
144
|
+
public getActiveSessions(): Session[] {
|
|
145
|
+
const sessions: Session[] = [];
|
|
146
|
+
|
|
147
|
+
for (const device of this.getActiveDevices()) {
|
|
148
|
+
if (device.activeSession) {
|
|
149
|
+
sessions.push(device.activeSession);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Sort by ability to send messages (prioritize ready sessions)
|
|
154
|
+
sessions.sort((a, b) => {
|
|
155
|
+
const aCanSend = !!(a.state?.theirNextNostrPublicKey && a.state?.ourCurrentNostrKey);
|
|
156
|
+
const bCanSend = !!(b.state?.theirNextNostrPublicKey && b.state?.ourCurrentNostrKey);
|
|
157
|
+
|
|
158
|
+
if (aCanSend && !bCanSend) return -1;
|
|
159
|
+
if (!aCanSend && bCanSend) return 1;
|
|
160
|
+
return 0;
|
|
161
|
+
});
|
|
100
162
|
|
|
101
|
-
return
|
|
102
|
-
.filter(([, record]) => !record.isStale && record.activeSession)
|
|
103
|
-
.map(([deviceId, record]) => [deviceId, record.activeSession!]);
|
|
163
|
+
return sessions;
|
|
104
164
|
}
|
|
105
165
|
|
|
106
166
|
/**
|
|
107
|
-
*
|
|
167
|
+
* Gets all sessions (active + inactive) across all devices
|
|
108
168
|
*/
|
|
109
|
-
public
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (!record) {
|
|
118
|
-
throw new Error(`No device record found for ${deviceId}`);
|
|
169
|
+
public getAllSessions(): Session[] {
|
|
170
|
+
const sessions: Session[] = [];
|
|
171
|
+
|
|
172
|
+
for (const device of this.deviceRecords.values()) {
|
|
173
|
+
if (device.activeSession) {
|
|
174
|
+
sessions.push(device.activeSession);
|
|
175
|
+
}
|
|
176
|
+
sessions.push(...device.inactiveSessions);
|
|
119
177
|
}
|
|
120
178
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
179
|
+
return sessions;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Gets sessions that are ready to send messages
|
|
184
|
+
*/
|
|
185
|
+
public getSendableSessions(): Session[] {
|
|
186
|
+
return this.getActiveSessions().filter(session =>
|
|
187
|
+
!!(session.state?.theirNextNostrPublicKey && session.state?.ourCurrentNostrKey)
|
|
128
188
|
);
|
|
189
|
+
}
|
|
129
190
|
|
|
130
|
-
|
|
131
|
-
|
|
191
|
+
// ============================================================================
|
|
192
|
+
// Stale Management
|
|
193
|
+
// ============================================================================
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Marks a specific device as stale
|
|
197
|
+
*/
|
|
198
|
+
public markDeviceStale(deviceId: string): void {
|
|
199
|
+
const device = this.deviceRecords.get(deviceId);
|
|
200
|
+
if (device) {
|
|
201
|
+
device.isStale = true;
|
|
202
|
+
device.staleTimestamp = Date.now();
|
|
203
|
+
}
|
|
132
204
|
}
|
|
133
205
|
|
|
134
206
|
/**
|
|
135
|
-
*
|
|
207
|
+
* Marks the entire user record as stale
|
|
208
|
+
*/
|
|
209
|
+
public markUserStale(): void {
|
|
210
|
+
this.isStale = true;
|
|
211
|
+
this.staleTimestamp = Date.now();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Removes stale devices and sessions older than maxLatency
|
|
136
216
|
*/
|
|
137
217
|
public pruneStaleRecords(maxLatency: number): void {
|
|
138
218
|
const now = Date.now();
|
|
139
219
|
|
|
140
|
-
//
|
|
141
|
-
for (const [deviceId,
|
|
142
|
-
if (
|
|
143
|
-
(now -
|
|
144
|
-
|
|
145
|
-
record.activeSession?.close();
|
|
146
|
-
record.inactiveSessions.forEach(session => session.close());
|
|
147
|
-
this.deviceRecords.delete(deviceId);
|
|
220
|
+
// Remove stale devices
|
|
221
|
+
for (const [deviceId, device] of this.deviceRecords.entries()) {
|
|
222
|
+
if (device.isStale && device.staleTimestamp &&
|
|
223
|
+
(now - device.staleTimestamp) > maxLatency) {
|
|
224
|
+
this.removeDevice(deviceId);
|
|
148
225
|
}
|
|
149
226
|
}
|
|
150
227
|
|
|
151
|
-
//
|
|
228
|
+
// Remove entire user record if stale
|
|
152
229
|
if (this.isStale && this.staleTimestamp &&
|
|
153
230
|
(now - this.staleTimestamp) > maxLatency) {
|
|
154
|
-
this.
|
|
155
|
-
record.activeSession?.close();
|
|
156
|
-
record.inactiveSessions.forEach(session => session.close());
|
|
157
|
-
});
|
|
158
|
-
this.deviceRecords.clear();
|
|
231
|
+
this.close();
|
|
159
232
|
}
|
|
160
233
|
}
|
|
161
234
|
|
|
235
|
+
// ============================================================================
|
|
236
|
+
// Utility Methods
|
|
237
|
+
// ============================================================================
|
|
238
|
+
|
|
162
239
|
/**
|
|
163
|
-
*
|
|
240
|
+
* Gets the most recently active device
|
|
164
241
|
*/
|
|
165
|
-
public
|
|
166
|
-
this.
|
|
167
|
-
|
|
168
|
-
|
|
242
|
+
public getMostActiveDevice(): DeviceRecord | undefined {
|
|
243
|
+
const activeDevices = this.getActiveDevices();
|
|
244
|
+
if (activeDevices.length === 0) return undefined;
|
|
245
|
+
|
|
246
|
+
return activeDevices.reduce((most, current) => {
|
|
247
|
+
const mostActivity = most.lastActivity || 0;
|
|
248
|
+
const currentActivity = current.lastActivity || 0;
|
|
249
|
+
return currentActivity > mostActivity ? current : most;
|
|
169
250
|
});
|
|
170
|
-
this.deviceRecords.clear();
|
|
171
251
|
}
|
|
172
252
|
|
|
173
|
-
// ---------------------------------------------------------------------------
|
|
174
|
-
// Helper methods used by SessionManager (WIP):
|
|
175
|
-
// ---------------------------------------------------------------------------
|
|
176
|
-
|
|
177
253
|
/**
|
|
178
|
-
*
|
|
179
|
-
* For now this means any session in a non-stale device record as well as
|
|
180
|
-
* all sessions added through `addSession`.
|
|
181
|
-
* Prioritizes initiator sessions (can send first message) over responder sessions.
|
|
254
|
+
* Gets the preferred session (from most active device)
|
|
182
255
|
*/
|
|
183
|
-
public
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (!record.isStale && record.activeSession) {
|
|
188
|
-
sessions.push(record.activeSession);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
256
|
+
public getPreferredSession(): Session | undefined {
|
|
257
|
+
const mostActive = this.getMostActiveDevice();
|
|
258
|
+
return mostActive?.activeSession;
|
|
259
|
+
}
|
|
191
260
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
return 0; // equal priority
|
|
199
|
-
});
|
|
261
|
+
/**
|
|
262
|
+
* Checks if this user has any active sessions
|
|
263
|
+
*/
|
|
264
|
+
public hasActiveSessions(): boolean {
|
|
265
|
+
return this.getActiveSessions().length > 0;
|
|
266
|
+
}
|
|
200
267
|
|
|
201
|
-
|
|
268
|
+
/**
|
|
269
|
+
* Gets total count of devices
|
|
270
|
+
*/
|
|
271
|
+
public getDeviceCount(): number {
|
|
272
|
+
return this.deviceRecords.size;
|
|
202
273
|
}
|
|
203
274
|
|
|
204
275
|
/**
|
|
205
|
-
*
|
|
206
|
-
* user. This is required for `SessionManager.onEvent` so that it can attach
|
|
207
|
-
* listeners to existing sessions.
|
|
276
|
+
* Gets total count of active sessions
|
|
208
277
|
*/
|
|
209
|
-
public
|
|
210
|
-
|
|
278
|
+
public getActiveSessionCount(): number {
|
|
279
|
+
return this.getActiveSessions().length;
|
|
280
|
+
}
|
|
211
281
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
282
|
+
/**
|
|
283
|
+
* Cleanup when destroying the user record
|
|
284
|
+
*/
|
|
285
|
+
public close(): void {
|
|
286
|
+
for (const device of this.deviceRecords.values()) {
|
|
287
|
+
device.activeSession?.close();
|
|
288
|
+
device.inactiveSessions.forEach(session => session.close());
|
|
217
289
|
}
|
|
290
|
+
this.deviceRecords.clear();
|
|
291
|
+
}
|
|
218
292
|
|
|
219
|
-
|
|
293
|
+
// ============================================================================
|
|
294
|
+
// Legacy Compatibility Methods
|
|
295
|
+
// ============================================================================
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* @deprecated Use upsertDevice instead
|
|
299
|
+
*/
|
|
300
|
+
public conditionalUpdate(deviceId: string, publicKey: string): void {
|
|
301
|
+
this.upsertDevice(deviceId, publicKey);
|
|
220
302
|
}
|
|
221
303
|
|
|
222
304
|
/**
|
|
223
|
-
*
|
|
224
|
-
* (if deviceId provided **and** the record exists) or falls back to the
|
|
225
|
-
* legacy extraSessions list.
|
|
305
|
+
* @deprecated Use upsertSession instead
|
|
226
306
|
*/
|
|
227
|
-
public
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
307
|
+
public insertSession(deviceId: string, session: Session): void {
|
|
308
|
+
this.upsertSession(deviceId, session);
|
|
309
|
+
}
|
|
231
310
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
311
|
+
/**
|
|
312
|
+
* Creates a new session for a device
|
|
313
|
+
*/
|
|
314
|
+
public createSession(
|
|
315
|
+
deviceId: string,
|
|
316
|
+
sharedSecret: Uint8Array,
|
|
317
|
+
ourCurrentPrivateKey: Uint8Array,
|
|
318
|
+
isInitiator: boolean,
|
|
319
|
+
name?: string
|
|
320
|
+
): Session {
|
|
321
|
+
const device = this.getDevice(deviceId);
|
|
322
|
+
if (!device) {
|
|
323
|
+
throw new Error(`No device record found for ${deviceId}`);
|
|
240
324
|
}
|
|
241
325
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
326
|
+
const session = Session.init(
|
|
327
|
+
this.nostrSubscribe,
|
|
328
|
+
device.publicKey,
|
|
329
|
+
ourCurrentPrivateKey,
|
|
330
|
+
isInitiator,
|
|
331
|
+
sharedSecret,
|
|
332
|
+
name || deviceId
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
this.upsertSession(deviceId, session);
|
|
336
|
+
return session;
|
|
248
337
|
}
|
|
249
338
|
}
|