nostr-double-ratchet 0.0.14 → 0.0.15

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/Session.ts CHANGED
@@ -1,17 +1,22 @@
1
- import { generateSecretKey, getPublicKey, nip44, finalizeEvent, VerifiedEvent } from "nostr-tools";
1
+ import { generateSecretKey, getPublicKey, nip44, finalizeEvent, VerifiedEvent, UnsignedEvent, getEventHash } from "nostr-tools";
2
2
  import { bytesToHex } from "@noble/hashes/utils";
3
3
  import {
4
4
  SessionState,
5
5
  Header,
6
6
  Unsubscribe,
7
7
  NostrSubscribe,
8
- MessageCallback,
8
+ EventCallback,
9
9
  MESSAGE_EVENT_KIND,
10
+ Rumor,
11
+ CHAT_MESSAGE_KIND,
10
12
  } from "./types";
11
13
  import { kdf, skippedMessageIndexKey } from "./utils";
12
14
 
13
15
  const MAX_SKIP = 1000;
14
16
 
17
+ // 64 zeros
18
+ const DUMMY_PUBKEY = '0000000000000000000000000000000000000000000000000000000000000000'
19
+
15
20
  /**
16
21
  * Double ratchet secure communication session over Nostr
17
22
  *
@@ -21,7 +26,7 @@ const MAX_SKIP = 1000;
21
26
  export class Session {
22
27
  private nostrUnsubscribe?: Unsubscribe;
23
28
  private nostrNextUnsubscribe?: Unsubscribe;
24
- private internalSubscriptions = new Map<number, MessageCallback>();
29
+ private internalSubscriptions = new Map<number, EventCallback>();
25
30
  private currentInternalSubscriptionId = 0;
26
31
  public name: string;
27
32
 
@@ -70,17 +75,49 @@ export class Session {
70
75
  }
71
76
 
72
77
  /**
73
- * Sends an encrypted message through the session
74
- * @param data The plaintext message to send
78
+ * Sends a text message through the encrypted session
79
+ * @param text The plaintext message to send
80
+ * @returns A verified Nostr event containing the encrypted message
81
+ * @throws Error if we are not the initiator and trying to send the first message
82
+ */
83
+ send(text: string): VerifiedEvent {
84
+ return this.sendEvent({
85
+ content: text,
86
+ kind: CHAT_MESSAGE_KIND
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Send a Nostr event through the encrypted session
92
+ * @param event Partial Nostr event to send. Must be unsigned. Id and will be generated if not provided.
75
93
  * @returns A verified Nostr event containing the encrypted message
76
94
  * @throws Error if we are not the initiator and trying to send the first message
77
95
  */
78
- send(data: string): VerifiedEvent {
96
+ sendEvent(event: Partial<UnsignedEvent>): VerifiedEvent {
79
97
  if (!this.state.theirNextNostrPublicKey || !this.state.ourCurrentNostrKey) {
80
98
  throw new Error("we are not the initiator, so we can't send the first message");
81
99
  }
82
100
 
83
- const [header, encryptedData] = this.ratchetEncrypt(data);
101
+ if ("sig" in event) {
102
+ throw new Error("Event must be unsigned " + JSON.stringify(event));
103
+ }
104
+
105
+ const rumor: Partial<Rumor> = {
106
+ ...event,
107
+ content: event.content || "",
108
+ kind: event.kind || MESSAGE_EVENT_KIND,
109
+ created_at: event.created_at || Math.floor(Date.now() / 1000),
110
+ tags: event.tags || [],
111
+ pubkey: event.pubkey || DUMMY_PUBKEY,
112
+ }
113
+
114
+ if (!rumor.tags!.some(([k]) => k === "ms")) {
115
+ rumor.tags!.push(["ms", Date.now().toString()])
116
+ }
117
+
118
+ rumor.id = getEventHash(rumor as Rumor);
119
+
120
+ const [header, encryptedData] = this.ratchetEncrypt(JSON.stringify(event));
84
121
 
85
122
  const sharedSecret = nip44.getConversationKey(this.state.ourCurrentNostrKey.privateKey, this.state.theirNextNostrPublicKey);
86
123
  const encryptedHeader = nip44.encrypt(JSON.stringify(header), sharedSecret);
@@ -100,7 +137,7 @@ export class Session {
100
137
  * @param callback Function to be called when a message is received
101
138
  * @returns Unsubscribe function to stop receiving messages
102
139
  */
103
- onMessage(callback: MessageCallback): Unsubscribe {
140
+ onEvent(callback: EventCallback): Unsubscribe {
104
141
  const id = this.currentInternalSubscriptionId++
105
142
  this.internalSubscriptions.set(id, callback)
106
143
  this.subscribeToNostrEvents()
@@ -122,7 +159,6 @@ export class Session {
122
159
  const header: Header = {
123
160
  number: this.state.sendingChainMessageNumber++,
124
161
  nextPublicKey: this.state.ourNextNostrKey.publicKey,
125
- time: Date.now(),
126
162
  previousChainLength: this.state.previousSendingChainMessageCount
127
163
  };
128
164
  return [header, nip44.encrypt(plaintext, messageKey)];
@@ -283,9 +319,10 @@ export class Session {
283
319
  }
284
320
  }
285
321
 
286
- const data = this.ratchetDecrypt(header, e.content, e.pubkey);
322
+ const text = this.ratchetDecrypt(header, e.content, e.pubkey);
323
+ const innerEvent = JSON.parse(text);
287
324
 
288
- this.internalSubscriptions.forEach(callback => callback({id: e.id, data, pubkey: header.nextPublicKey, time: header.time}));
325
+ this.internalSubscriptions.forEach(callback => callback(innerEvent, e));
289
326
  }
290
327
 
291
328
  private subscribeToNostrEvents() {
package/src/types.ts CHANGED
@@ -1,20 +1,9 @@
1
1
  import { Filter, UnsignedEvent, VerifiedEvent } from "nostr-tools";
2
2
 
3
- /**
4
- * An event that has been verified to be from the Nostr network.
5
- */
6
- export type Message = {
7
- id: string;
8
- data: string;
9
- pubkey: string;
10
- time: number; // unlike Nostr, we use milliseconds instead of seconds
11
- }
12
-
13
3
  export type Header = {
14
4
  number: number;
15
5
  previousChainLength: number;
16
6
  nextPublicKey: string;
17
- time: number;
18
7
  }
19
8
 
20
9
  /**
@@ -76,14 +65,26 @@ export type Unsubscribe = () => void;
76
65
  */
77
66
  export type NostrSubscribe = (filter: Filter, onEvent: (e: VerifiedEvent) => void) => Unsubscribe;
78
67
 
68
+ export type Rumor = UnsignedEvent & { id: string }
69
+
79
70
  /**
80
71
  * Callback function for handling decrypted messages
81
72
  * @param message - The decrypted message object
82
73
  */
83
- export type MessageCallback = (message: Message) => void;
74
+ export type EventCallback = (event: Rumor, outerEvent: VerifiedEvent) => void;
75
+
76
+ /**
77
+ * Message event kind
78
+ */
79
+ export const MESSAGE_EVENT_KIND = 30078;
84
80
 
85
- export const MESSAGE_EVENT_KIND = 4;
81
+ /**
82
+ * Invite event kind
83
+ */
86
84
  export const INVITE_EVENT_KIND = 30078;
85
+
86
+ export const CHAT_MESSAGE_KIND = 14;
87
+
87
88
  export const MAX_SKIP = 100;
88
89
 
89
90
  export type NostrEvent = {
@@ -96,11 +97,6 @@ export type NostrEvent = {
96
97
  sig: string;
97
98
  }
98
99
 
99
- export enum Sender {
100
- Us,
101
- Them
102
- }
103
-
104
100
  export type EncryptFunction = (plaintext: string, pubkey: string) => Promise<string>;
105
101
  export type DecryptFunction = (ciphertext: string, pubkey: string) => Promise<string>;
106
102
  export type NostrPublish = (event: UnsignedEvent) => Promise<VerifiedEvent>;
package/src/utils.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { hexToBytes, bytesToHex } from "@noble/hashes/utils";
2
- import { SessionState, Message } from "./types";
2
+ import { Rumor, SessionState } 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';
@@ -71,16 +71,16 @@ export function deserializeSessionState(data: string): SessionState {
71
71
  };
72
72
  }
73
73
 
74
- export async function* createMessageStream(session: Session): AsyncGenerator<Message, void, unknown> {
75
- const messageQueue: Message[] = [];
76
- let resolveNext: ((value: Message) => void) | null = null;
74
+ export async function* createEventStream(session: Session): AsyncGenerator<Rumor, void, unknown> {
75
+ const messageQueue: Rumor[] = [];
76
+ let resolveNext: ((value: Rumor) => void) | null = null;
77
77
 
78
- const unsubscribe = session.onMessage((message) => {
78
+ const unsubscribe = session.onEvent((event) => {
79
79
  if (resolveNext) {
80
- resolveNext(message);
80
+ resolveNext(event);
81
81
  resolveNext = null;
82
82
  } else {
83
- messageQueue.push(message);
83
+ messageQueue.push(event);
84
84
  }
85
85
  });
86
86
 
@@ -89,7 +89,7 @@ export async function* createMessageStream(session: Session): AsyncGenerator<Mes
89
89
  if (messageQueue.length > 0) {
90
90
  yield messageQueue.shift()!;
91
91
  } else {
92
- yield new Promise<Message>(resolve => {
92
+ yield new Promise<Rumor>(resolve => {
93
93
  resolveNext = resolve;
94
94
  });
95
95
  }