nostr-double-ratchet 0.0.37 → 0.0.48
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 +52 -15
- package/dist/AppKeys.d.ts +52 -0
- package/dist/AppKeys.d.ts.map +1 -0
- package/dist/AppKeysManager.d.ts +136 -0
- package/dist/AppKeysManager.d.ts.map +1 -0
- package/dist/Invite.d.ts +7 -7
- package/dist/Invite.d.ts.map +1 -1
- package/dist/Session.d.ts +29 -0
- package/dist/Session.d.ts.map +1 -1
- package/dist/SessionManager.d.ts +46 -22
- package/dist/SessionManager.d.ts.map +1 -1
- package/dist/StorageAdapter.d.ts.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/inviteUtils.d.ts +122 -0
- package/dist/inviteUtils.d.ts.map +1 -0
- package/dist/nostr-double-ratchet.es.js +2843 -1901
- package/dist/nostr-double-ratchet.umd.js +1 -1
- package/dist/types.d.ts +32 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +20 -2
- package/dist/utils.d.ts.map +1 -1
- package/package.json +5 -19
- package/src/AppKeys.ts +210 -0
- package/src/AppKeysManager.ts +405 -0
- package/src/Invite.ts +69 -89
- package/src/Session.ts +47 -4
- package/src/SessionManager.ts +478 -300
- package/src/StorageAdapter.ts +5 -6
- package/src/index.ts +4 -1
- package/src/inviteUtils.ts +271 -0
- package/src/types.ts +37 -2
- package/src/utils.ts +45 -8
- package/LICENSE +0 -21
- package/dist/UserRecord.d.ts +0 -117
- package/dist/UserRecord.d.ts.map +0 -1
- package/src/UserRecord.ts +0 -338
package/src/StorageAdapter.ts
CHANGED
|
@@ -58,8 +58,7 @@ export class LocalStorageAdapter implements StorageAdapter {
|
|
|
58
58
|
try {
|
|
59
59
|
const item = localStorage.getItem(this.getFullKey(key))
|
|
60
60
|
return item ? JSON.parse(item) : undefined
|
|
61
|
-
} catch
|
|
62
|
-
console.warn(`Failed to get key ${key} from localStorage:`, e)
|
|
61
|
+
} catch {
|
|
63
62
|
return undefined
|
|
64
63
|
}
|
|
65
64
|
}
|
|
@@ -76,8 +75,8 @@ export class LocalStorageAdapter implements StorageAdapter {
|
|
|
76
75
|
async del(key: string): Promise<void> {
|
|
77
76
|
try {
|
|
78
77
|
localStorage.removeItem(this.getFullKey(key))
|
|
79
|
-
} catch
|
|
80
|
-
|
|
78
|
+
} catch {
|
|
79
|
+
// Ignore deletion failures
|
|
81
80
|
}
|
|
82
81
|
}
|
|
83
82
|
|
|
@@ -93,8 +92,8 @@ export class LocalStorageAdapter implements StorageAdapter {
|
|
|
93
92
|
keys.push(key.substring(this.keyPrefix.length))
|
|
94
93
|
}
|
|
95
94
|
}
|
|
96
|
-
} catch
|
|
97
|
-
|
|
95
|
+
} catch {
|
|
96
|
+
// Ignore list failures
|
|
98
97
|
}
|
|
99
98
|
|
|
100
99
|
return keys
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export * from "./Session"
|
|
2
2
|
export * from "./Invite"
|
|
3
|
+
export * from "./AppKeys"
|
|
4
|
+
export * from "./inviteUtils"
|
|
3
5
|
export * from "./types"
|
|
4
6
|
export * from "./utils"
|
|
5
7
|
export * from "./SessionManager"
|
|
6
|
-
export * from "./
|
|
8
|
+
export * from "./AppKeysManager"
|
|
9
|
+
export * from "./StorageAdapter"
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { generateSecretKey, getPublicKey, nip44 } from 'nostr-tools'
|
|
2
|
+
import { getConversationKey } from 'nostr-tools/nip44'
|
|
3
|
+
import { hexToBytes, bytesToHex } from '@noble/hashes/utils'
|
|
4
|
+
import { Session } from './Session'
|
|
5
|
+
import { NostrSubscribe, INVITE_RESPONSE_KIND, EncryptFunction, DecryptFunction, KeyPair } from './types'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Device payload for QR code / text code sharing.
|
|
9
|
+
*/
|
|
10
|
+
export interface DevicePayload {
|
|
11
|
+
/** Ephemeral public key (64 hex chars) */
|
|
12
|
+
ephemeralPubkey: string
|
|
13
|
+
/** Shared secret (64 hex chars) */
|
|
14
|
+
sharedSecret: string
|
|
15
|
+
/** Device ID (16 hex chars) */
|
|
16
|
+
deviceId: string
|
|
17
|
+
/** Human-readable device label */
|
|
18
|
+
deviceLabel: string
|
|
19
|
+
/** Identity public key for this device (64 hex chars) */
|
|
20
|
+
identityPubkey: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generates a new ephemeral keypair for invites.
|
|
25
|
+
* @returns A keypair with publicKey (hex string) and privateKey (Uint8Array)
|
|
26
|
+
*/
|
|
27
|
+
export function generateEphemeralKeypair(): KeyPair {
|
|
28
|
+
const privateKey = generateSecretKey()
|
|
29
|
+
const publicKey = getPublicKey(privateKey)
|
|
30
|
+
return { publicKey, privateKey }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Generates a new shared secret for invite handshakes.
|
|
35
|
+
* @returns A 64-character hex string (32 bytes)
|
|
36
|
+
*/
|
|
37
|
+
export function generateSharedSecret(): string {
|
|
38
|
+
return bytesToHex(generateSecretKey())
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Generates a unique device ID.
|
|
43
|
+
* @returns A random device ID string
|
|
44
|
+
*/
|
|
45
|
+
export function generateDeviceId(): string {
|
|
46
|
+
return bytesToHex(generateSecretKey()).slice(0, 16)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface EncryptInviteResponseParams {
|
|
50
|
+
/** The invitee's session public key */
|
|
51
|
+
inviteeSessionPublicKey: string
|
|
52
|
+
/** The invitee's identity public key (also serves as device ID) */
|
|
53
|
+
inviteePublicKey: string
|
|
54
|
+
/** The invitee's identity private key (optional if encrypt function provided) */
|
|
55
|
+
inviteePrivateKey?: Uint8Array
|
|
56
|
+
/** The inviter's identity public key */
|
|
57
|
+
inviterPublicKey: string
|
|
58
|
+
/** The inviter's ephemeral public key */
|
|
59
|
+
inviterEphemeralPublicKey: string
|
|
60
|
+
/** The shared secret for the invite */
|
|
61
|
+
sharedSecret: string
|
|
62
|
+
/** The invitee's owner/Nostr identity public key (optional for single-device users) */
|
|
63
|
+
ownerPublicKey?: string
|
|
64
|
+
/** Optional custom encrypt function */
|
|
65
|
+
encrypt?: EncryptFunction
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface EncryptedInviteResponse {
|
|
69
|
+
/** The inner event containing the encrypted payload */
|
|
70
|
+
innerEvent: {
|
|
71
|
+
pubkey: string
|
|
72
|
+
content: string
|
|
73
|
+
created_at: number
|
|
74
|
+
}
|
|
75
|
+
/** The outer envelope event */
|
|
76
|
+
envelope: {
|
|
77
|
+
kind: number
|
|
78
|
+
pubkey: string
|
|
79
|
+
content: string
|
|
80
|
+
created_at: number
|
|
81
|
+
tags: string[][]
|
|
82
|
+
}
|
|
83
|
+
/** The random sender's public key used for the envelope */
|
|
84
|
+
randomSenderPublicKey: string
|
|
85
|
+
/** The random sender's private key used for the envelope */
|
|
86
|
+
randomSenderPrivateKey: Uint8Array
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const TWO_DAYS = 2 * 24 * 60 * 60
|
|
90
|
+
const now = () => Math.round(Date.now() / 1000)
|
|
91
|
+
const randomNow = () => Math.round(now() - Math.random() * TWO_DAYS)
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Encrypts an invite response with two-layer encryption.
|
|
95
|
+
*
|
|
96
|
+
* Layer 1 (inner): Payload encrypted with DH key, then encrypted with shared secret.
|
|
97
|
+
* Layer 2 (outer): Envelope encrypted with random key -> inviter ephemeral key.
|
|
98
|
+
*/
|
|
99
|
+
export async function encryptInviteResponse(params: EncryptInviteResponseParams): Promise<EncryptedInviteResponse> {
|
|
100
|
+
const {
|
|
101
|
+
inviteeSessionPublicKey,
|
|
102
|
+
inviteePublicKey,
|
|
103
|
+
inviteePrivateKey,
|
|
104
|
+
inviterPublicKey,
|
|
105
|
+
inviterEphemeralPublicKey,
|
|
106
|
+
sharedSecret,
|
|
107
|
+
ownerPublicKey,
|
|
108
|
+
encrypt,
|
|
109
|
+
} = params
|
|
110
|
+
|
|
111
|
+
const sharedSecretBytes = hexToBytes(sharedSecret)
|
|
112
|
+
|
|
113
|
+
// Create the encrypt function
|
|
114
|
+
const encryptFn = encrypt ?? (async (plaintext: string, pubkey: string) => {
|
|
115
|
+
if (!inviteePrivateKey) {
|
|
116
|
+
throw new Error('inviteePrivateKey is required when encrypt function is not provided')
|
|
117
|
+
}
|
|
118
|
+
return nip44.encrypt(plaintext, getConversationKey(inviteePrivateKey, pubkey))
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
// Create the payload
|
|
122
|
+
// Note: deviceId is no longer needed - inviteePublicKey (identity) serves as device ID
|
|
123
|
+
const payload = JSON.stringify({
|
|
124
|
+
sessionKey: inviteeSessionPublicKey,
|
|
125
|
+
...(ownerPublicKey && { ownerPublicKey }),
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
// Encrypt with DH key (invitee -> inviter)
|
|
129
|
+
const dhEncrypted = await encryptFn(payload, inviterPublicKey)
|
|
130
|
+
|
|
131
|
+
// Encrypt with shared secret
|
|
132
|
+
const innerEvent = {
|
|
133
|
+
pubkey: inviteePublicKey,
|
|
134
|
+
content: nip44.encrypt(dhEncrypted, sharedSecretBytes),
|
|
135
|
+
created_at: now(),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Create a random keypair for the envelope sender
|
|
139
|
+
const randomSenderPrivateKey = generateSecretKey()
|
|
140
|
+
const randomSenderPublicKey = getPublicKey(randomSenderPrivateKey)
|
|
141
|
+
|
|
142
|
+
// Encrypt the inner event with the random key -> inviter ephemeral key
|
|
143
|
+
const innerJson = JSON.stringify(innerEvent)
|
|
144
|
+
const envelope = {
|
|
145
|
+
kind: INVITE_RESPONSE_KIND,
|
|
146
|
+
pubkey: randomSenderPublicKey,
|
|
147
|
+
content: nip44.encrypt(innerJson, getConversationKey(randomSenderPrivateKey, inviterEphemeralPublicKey)),
|
|
148
|
+
created_at: randomNow(),
|
|
149
|
+
tags: [['p', inviterEphemeralPublicKey]],
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
innerEvent,
|
|
154
|
+
envelope,
|
|
155
|
+
randomSenderPublicKey,
|
|
156
|
+
randomSenderPrivateKey,
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface DecryptInviteResponseParams {
|
|
161
|
+
/** The encrypted envelope content */
|
|
162
|
+
envelopeContent: string
|
|
163
|
+
/** The envelope sender's public key */
|
|
164
|
+
envelopeSenderPubkey: string
|
|
165
|
+
/** The inviter's ephemeral private key */
|
|
166
|
+
inviterEphemeralPrivateKey: Uint8Array
|
|
167
|
+
/** The inviter's identity private key (optional if decrypt function provided) */
|
|
168
|
+
inviterPrivateKey?: Uint8Array
|
|
169
|
+
/** The shared secret for the invite */
|
|
170
|
+
sharedSecret: string
|
|
171
|
+
/** Optional custom decrypt function */
|
|
172
|
+
decrypt?: DecryptFunction
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface DecryptedInviteResponse {
|
|
176
|
+
/** The invitee's identity public key (also serves as device ID) */
|
|
177
|
+
inviteeIdentity: string
|
|
178
|
+
/** The invitee's session public key */
|
|
179
|
+
inviteeSessionPublicKey: string
|
|
180
|
+
/** The invitee's owner/Nostr identity public key (optional for backward compat) */
|
|
181
|
+
ownerPublicKey?: string
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Decrypts an invite response.
|
|
186
|
+
*/
|
|
187
|
+
export async function decryptInviteResponse(params: DecryptInviteResponseParams): Promise<DecryptedInviteResponse> {
|
|
188
|
+
const {
|
|
189
|
+
envelopeContent,
|
|
190
|
+
envelopeSenderPubkey,
|
|
191
|
+
inviterEphemeralPrivateKey,
|
|
192
|
+
inviterPrivateKey,
|
|
193
|
+
sharedSecret,
|
|
194
|
+
decrypt,
|
|
195
|
+
} = params
|
|
196
|
+
|
|
197
|
+
const sharedSecretBytes = hexToBytes(sharedSecret)
|
|
198
|
+
|
|
199
|
+
// Decrypt the outer envelope
|
|
200
|
+
const decrypted = nip44.decrypt(
|
|
201
|
+
envelopeContent,
|
|
202
|
+
getConversationKey(inviterEphemeralPrivateKey, envelopeSenderPubkey)
|
|
203
|
+
)
|
|
204
|
+
const innerEvent = JSON.parse(decrypted)
|
|
205
|
+
|
|
206
|
+
const inviteeIdentity = innerEvent.pubkey
|
|
207
|
+
|
|
208
|
+
// Decrypt the inner content using shared secret
|
|
209
|
+
const dhEncrypted = nip44.decrypt(innerEvent.content, sharedSecretBytes)
|
|
210
|
+
|
|
211
|
+
// Create the decrypt function
|
|
212
|
+
const decryptFn = decrypt ?? (async (ciphertext: string, pubkey: string) => {
|
|
213
|
+
if (!inviterPrivateKey) {
|
|
214
|
+
throw new Error('inviterPrivateKey is required when decrypt function is not provided')
|
|
215
|
+
}
|
|
216
|
+
return nip44.decrypt(ciphertext, getConversationKey(inviterPrivateKey, pubkey))
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
// Decrypt using DH key
|
|
220
|
+
const decryptedPayload = await decryptFn(dhEncrypted, inviteeIdentity)
|
|
221
|
+
|
|
222
|
+
let inviteeSessionPublicKey: string
|
|
223
|
+
let ownerPublicKey: string | undefined
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
const parsed = JSON.parse(decryptedPayload)
|
|
227
|
+
inviteeSessionPublicKey = parsed.sessionKey
|
|
228
|
+
ownerPublicKey = parsed.ownerPublicKey
|
|
229
|
+
} catch {
|
|
230
|
+
// Backward compatibility: plain session key
|
|
231
|
+
inviteeSessionPublicKey = decryptedPayload
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
inviteeIdentity,
|
|
236
|
+
inviteeSessionPublicKey,
|
|
237
|
+
ownerPublicKey,
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export interface CreateSessionFromAcceptParams {
|
|
242
|
+
/** Nostr subscription function */
|
|
243
|
+
nostrSubscribe: NostrSubscribe
|
|
244
|
+
/** The other party's public key */
|
|
245
|
+
theirPublicKey: string
|
|
246
|
+
/** Our session private key */
|
|
247
|
+
ourSessionPrivateKey: Uint8Array
|
|
248
|
+
/** The shared secret (hex string) */
|
|
249
|
+
sharedSecret: string
|
|
250
|
+
/** Whether we are the sender (initiator) */
|
|
251
|
+
isSender: boolean
|
|
252
|
+
/** Optional session name */
|
|
253
|
+
name?: string
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Creates a Session from invite acceptance parameters.
|
|
258
|
+
*/
|
|
259
|
+
export function createSessionFromAccept(params: CreateSessionFromAcceptParams): Session {
|
|
260
|
+
const {
|
|
261
|
+
nostrSubscribe,
|
|
262
|
+
theirPublicKey,
|
|
263
|
+
ourSessionPrivateKey,
|
|
264
|
+
sharedSecret,
|
|
265
|
+
isSender,
|
|
266
|
+
name,
|
|
267
|
+
} = params
|
|
268
|
+
|
|
269
|
+
const sharedSecretBytes = hexToBytes(sharedSecret)
|
|
270
|
+
return Session.init(nostrSubscribe, theirPublicKey, ourSessionPrivateKey, isSender, sharedSecretBytes, name)
|
|
271
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -68,11 +68,18 @@ export type Unsubscribe = () => void;
|
|
|
68
68
|
export type NostrSubscribe = (_filter: Filter, _onEvent: (_e: VerifiedEvent) => void) => Unsubscribe;
|
|
69
69
|
export type EncryptFunction = (_plaintext: string, _pubkey: string) => Promise<string>;
|
|
70
70
|
export type DecryptFunction = (_ciphertext: string, _pubkey: string) => Promise<string>;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Identity key for cryptographic operations.
|
|
74
|
+
* Either a raw private key (Uint8Array) or encrypt/decrypt functions for extension login (NIP-07).
|
|
75
|
+
*/
|
|
76
|
+
export type IdentityKey = Uint8Array | { encrypt: EncryptFunction; decrypt: DecryptFunction };
|
|
77
|
+
|
|
71
78
|
export type NostrPublish = (_event: UnsignedEvent) => Promise<VerifiedEvent>;
|
|
72
79
|
|
|
73
80
|
export type Rumor = UnsignedEvent & { id: string }
|
|
74
81
|
|
|
75
|
-
/**
|
|
82
|
+
/**
|
|
76
83
|
* Callback function for handling decrypted messages
|
|
77
84
|
* @param _event - The decrypted message object (Rumor)
|
|
78
85
|
* @param _outerEvent - The outer Nostr event (VerifiedEvent)
|
|
@@ -91,9 +98,10 @@ export const INVITE_EVENT_KIND = 30078;
|
|
|
91
98
|
|
|
92
99
|
export const INVITE_RESPONSE_KIND = 1059;
|
|
93
100
|
|
|
101
|
+
export const APP_KEYS_EVENT_KIND = 30078;
|
|
102
|
+
|
|
94
103
|
export const CHAT_MESSAGE_KIND = 14;
|
|
95
104
|
|
|
96
|
-
export const MAX_SKIP = 100;
|
|
97
105
|
|
|
98
106
|
export type NostrEvent = {
|
|
99
107
|
id: string;
|
|
@@ -104,3 +112,30 @@ export type NostrEvent = {
|
|
|
104
112
|
content: string;
|
|
105
113
|
sig: string;
|
|
106
114
|
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Payload for reaction messages sent through NDR.
|
|
118
|
+
* Reactions are regular messages with a JSON payload indicating they're a reaction.
|
|
119
|
+
*/
|
|
120
|
+
export interface ReactionPayload {
|
|
121
|
+
type: 'reaction';
|
|
122
|
+
/** ID of the message being reacted to */
|
|
123
|
+
messageId: string;
|
|
124
|
+
/** Emoji or reaction content */
|
|
125
|
+
emoji: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Kind constant for reaction inner events
|
|
130
|
+
*/
|
|
131
|
+
export const REACTION_KIND = 7;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Kind constant for read/delivery receipt inner events
|
|
135
|
+
*/
|
|
136
|
+
export const RECEIPT_KIND = 15;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Kind constant for typing indicator inner events
|
|
140
|
+
*/
|
|
141
|
+
export const TYPING_KIND = 25;
|
package/src/utils.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { hexToBytes, bytesToHex } from "@noble/hashes/utils";
|
|
2
|
-
import { Rumor, SessionState } from "./types";
|
|
2
|
+
import { Rumor, SessionState, ReactionPayload } from "./types";
|
|
3
3
|
import { Session } from "./Session.ts";
|
|
4
4
|
import { extract as hkdf_extract, expand as hkdf_expand } from '@noble/hashes/hkdf';
|
|
5
5
|
import { sha256 } from '@noble/hashes/sha256';
|
|
@@ -136,12 +136,12 @@ export function deepCopyState(s: SessionState): SessionState {
|
|
|
136
136
|
receivingChainMessageNumber: s.receivingChainMessageNumber,
|
|
137
137
|
previousSendingChainMessageCount: s.previousSendingChainMessageCount,
|
|
138
138
|
skippedKeys: Object.fromEntries(
|
|
139
|
-
Object.entries(s.skippedKeys).map(([author, entry]
|
|
139
|
+
Object.entries(s.skippedKeys).map(([author, entry]) => [
|
|
140
140
|
author,
|
|
141
141
|
{
|
|
142
|
-
headerKeys: entry.headerKeys.map((hk
|
|
142
|
+
headerKeys: entry.headerKeys.map((hk) => new Uint8Array(hk)),
|
|
143
143
|
messageKeys: Object.fromEntries(
|
|
144
|
-
Object.entries(entry.messageKeys).map(([n, mk]
|
|
144
|
+
Object.entries(entry.messageKeys).map(([n, mk]) => [n, new Uint8Array(mk)])
|
|
145
145
|
),
|
|
146
146
|
},
|
|
147
147
|
])
|
|
@@ -188,10 +188,6 @@ export function kdf(input1: Uint8Array, input2: Uint8Array = new Uint8Array(32),
|
|
|
188
188
|
return outputs;
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
export function skippedMessageIndexKey(_nostrSender: string, _number: number): string {
|
|
192
|
-
return `${_nostrSender}:${_number}`;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
191
|
export function getMillisecondTimestamp(event: Rumor) {
|
|
196
192
|
const msTag = event.tags?.find((tag: string[]) => tag[0] === "ms");
|
|
197
193
|
if (msTag) {
|
|
@@ -199,3 +195,44 @@ export function getMillisecondTimestamp(event: Rumor) {
|
|
|
199
195
|
}
|
|
200
196
|
return event.created_at * 1000;
|
|
201
197
|
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Check if a message content is a reaction payload.
|
|
201
|
+
* @param content The message content to check
|
|
202
|
+
* @returns The parsed ReactionPayload if valid, null otherwise
|
|
203
|
+
*/
|
|
204
|
+
export function parseReaction(content: string): ReactionPayload | null {
|
|
205
|
+
try {
|
|
206
|
+
const parsed = JSON.parse(content);
|
|
207
|
+
if (parsed.type === 'reaction' && typeof parsed.messageId === 'string' && typeof parsed.emoji === 'string') {
|
|
208
|
+
return parsed as ReactionPayload;
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
// Not JSON or invalid structure
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Check if a message content is a reaction.
|
|
218
|
+
* @param content The message content to check
|
|
219
|
+
* @returns true if the content is a reaction payload
|
|
220
|
+
*/
|
|
221
|
+
export function isReaction(content: string): boolean {
|
|
222
|
+
return parseReaction(content) !== null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Create a reaction payload JSON string.
|
|
227
|
+
* @param messageId The ID of the message being reacted to
|
|
228
|
+
* @param emoji The emoji or reaction content
|
|
229
|
+
* @returns JSON string of the reaction payload
|
|
230
|
+
*/
|
|
231
|
+
export function createReactionPayload(messageId: string, emoji: string): string {
|
|
232
|
+
const payload: ReactionPayload = {
|
|
233
|
+
type: 'reaction',
|
|
234
|
+
messageId,
|
|
235
|
+
emoji
|
|
236
|
+
};
|
|
237
|
+
return JSON.stringify(payload);
|
|
238
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2024 Martti Malmi
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
package/dist/UserRecord.d.ts
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { Session } from './Session';
|
|
2
|
-
import { NostrSubscribe } from './types';
|
|
3
|
-
interface DeviceRecord {
|
|
4
|
-
deviceId: string;
|
|
5
|
-
publicKey: string;
|
|
6
|
-
activeSession?: Session;
|
|
7
|
-
inactiveSessions: Session[];
|
|
8
|
-
isStale: boolean;
|
|
9
|
-
staleTimestamp?: number;
|
|
10
|
-
lastActivity?: number;
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Manages sessions for a single user across multiple devices
|
|
14
|
-
* Structure: UserRecord → DeviceRecord → Sessions
|
|
15
|
-
*/
|
|
16
|
-
export declare class UserRecord {
|
|
17
|
-
readonly userId: string;
|
|
18
|
-
private readonly nostrSubscribe;
|
|
19
|
-
private deviceRecords;
|
|
20
|
-
private isStale;
|
|
21
|
-
private staleTimestamp?;
|
|
22
|
-
constructor(userId: string, nostrSubscribe: NostrSubscribe);
|
|
23
|
-
/**
|
|
24
|
-
* Creates or updates a device record for this user
|
|
25
|
-
*/
|
|
26
|
-
upsertDevice(deviceId: string, publicKey: string): DeviceRecord;
|
|
27
|
-
/**
|
|
28
|
-
* Gets a device record by deviceId
|
|
29
|
-
*/
|
|
30
|
-
getDevice(deviceId: string): DeviceRecord | undefined;
|
|
31
|
-
/**
|
|
32
|
-
* Gets all device records for this user
|
|
33
|
-
*/
|
|
34
|
-
getAllDevices(): DeviceRecord[];
|
|
35
|
-
/**
|
|
36
|
-
* Gets all active (non-stale) device records
|
|
37
|
-
*/
|
|
38
|
-
getActiveDevices(): DeviceRecord[];
|
|
39
|
-
/**
|
|
40
|
-
* Removes a device record and closes all its sessions
|
|
41
|
-
*/
|
|
42
|
-
removeDevice(deviceId: string): boolean;
|
|
43
|
-
/**
|
|
44
|
-
* Adds or updates a session for a specific device
|
|
45
|
-
*/
|
|
46
|
-
upsertSession(deviceId: string, session: Session, publicKey?: string): void;
|
|
47
|
-
/**
|
|
48
|
-
* Gets the active session for a specific device
|
|
49
|
-
*/
|
|
50
|
-
getActiveSession(deviceId: string): Session | undefined;
|
|
51
|
-
/**
|
|
52
|
-
* Gets all sessions (active + inactive) for a specific device
|
|
53
|
-
*/
|
|
54
|
-
getDeviceSessions(deviceId: string): Session[];
|
|
55
|
-
/**
|
|
56
|
-
* Gets all active sessions across all devices for this user
|
|
57
|
-
*/
|
|
58
|
-
getActiveSessions(): Session[];
|
|
59
|
-
/**
|
|
60
|
-
* Gets all sessions (active + inactive) across all devices
|
|
61
|
-
*/
|
|
62
|
-
getAllSessions(): Session[];
|
|
63
|
-
/**
|
|
64
|
-
* Gets sessions that are ready to send messages
|
|
65
|
-
*/
|
|
66
|
-
getSendableSessions(): Session[];
|
|
67
|
-
/**
|
|
68
|
-
* Marks a specific device as stale
|
|
69
|
-
*/
|
|
70
|
-
markDeviceStale(deviceId: string): void;
|
|
71
|
-
/**
|
|
72
|
-
* Marks the entire user record as stale
|
|
73
|
-
*/
|
|
74
|
-
markUserStale(): void;
|
|
75
|
-
/**
|
|
76
|
-
* Removes stale devices and sessions older than maxLatency
|
|
77
|
-
*/
|
|
78
|
-
pruneStaleRecords(maxLatency: number): void;
|
|
79
|
-
/**
|
|
80
|
-
* Gets the most recently active device
|
|
81
|
-
*/
|
|
82
|
-
getMostActiveDevice(): DeviceRecord | undefined;
|
|
83
|
-
/**
|
|
84
|
-
* Gets the preferred session (from most active device)
|
|
85
|
-
*/
|
|
86
|
-
getPreferredSession(): Session | undefined;
|
|
87
|
-
/**
|
|
88
|
-
* Checks if this user has any active sessions
|
|
89
|
-
*/
|
|
90
|
-
hasActiveSessions(): boolean;
|
|
91
|
-
/**
|
|
92
|
-
* Gets total count of devices
|
|
93
|
-
*/
|
|
94
|
-
getDeviceCount(): number;
|
|
95
|
-
/**
|
|
96
|
-
* Gets total count of active sessions
|
|
97
|
-
*/
|
|
98
|
-
getActiveSessionCount(): number;
|
|
99
|
-
/**
|
|
100
|
-
* Cleanup when destroying the user record
|
|
101
|
-
*/
|
|
102
|
-
close(): void;
|
|
103
|
-
/**
|
|
104
|
-
* @deprecated Use upsertDevice instead
|
|
105
|
-
*/
|
|
106
|
-
conditionalUpdate(deviceId: string, publicKey: string): void;
|
|
107
|
-
/**
|
|
108
|
-
* @deprecated Use upsertSession instead
|
|
109
|
-
*/
|
|
110
|
-
insertSession(deviceId: string, session: Session): void;
|
|
111
|
-
/**
|
|
112
|
-
* Creates a new session for a device
|
|
113
|
-
*/
|
|
114
|
-
createSession(deviceId: string, sharedSecret: Uint8Array, ourCurrentPrivateKey: Uint8Array, isInitiator: boolean, name?: string): Session;
|
|
115
|
-
}
|
|
116
|
-
export {};
|
|
117
|
-
//# sourceMappingURL=UserRecord.d.ts.map
|
package/dist/UserRecord.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"UserRecord.d.ts","sourceRoot":"","sources":["../src/UserRecord.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAEzC,UAAU,YAAY;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,gBAAgB,EAAE,OAAO,EAAE,CAAC;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;GAGG;AACH,qBAAa,UAAU;aAMH,MAAM,EAAE,MAAM;IAC9B,OAAO,CAAC,QAAQ,CAAC,cAAc;IANjC,OAAO,CAAC,aAAa,CAAwC;IAC7D,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,cAAc,CAAC,CAAS;gBAGd,MAAM,EAAE,MAAM,EACb,cAAc,EAAE,cAAc;IAQjD;;OAEG;IACI,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,YAAY;IAyBtE;;OAEG;IACI,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS;IAI5D;;OAEG;IACI,aAAa,IAAI,YAAY,EAAE;IAItC;;OAEG;IACI,gBAAgB,IAAI,YAAY,EAAE;IAKzC;;OAEG;IACI,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAe9C;;OAEG;IACI,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;IAclF;;OAEG;IACI,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS;IAK9D;;OAEG;IACI,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,EAAE;IAYrD;;OAEG;IACI,iBAAiB,IAAI,OAAO,EAAE;IAsBrC;;OAEG;IACI,cAAc,IAAI,OAAO,EAAE;IAalC;;OAEG;IACI,mBAAmB,IAAI,OAAO,EAAE;IAUvC;;OAEG;IACI,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAQ9C;;OAEG;IACI,aAAa,IAAI,IAAI;IAK5B;;OAEG;IACI,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAsBlD;;OAEG;IACI,mBAAmB,IAAI,YAAY,GAAG,SAAS;IAWtD;;OAEG;IACI,mBAAmB,IAAI,OAAO,GAAG,SAAS;IAKjD;;OAEG;IACI,iBAAiB,IAAI,OAAO;IAInC;;OAEG;IACI,cAAc,IAAI,MAAM;IAI/B;;OAEG;IACI,qBAAqB,IAAI,MAAM;IAItC;;OAEG;IACI,KAAK,IAAI,IAAI;IAYpB;;OAEG;IACI,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAInE;;OAEG;IACI,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;IAI9D;;OAEG;IACI,aAAa,CAClB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,UAAU,EACxB,oBAAoB,EAAE,UAAU,EAChC,WAAW,EAAE,OAAO,EACpB,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO;CAkBX"}
|