@verbeth/sdk 0.1.4 → 0.1.6

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.
Files changed (190) hide show
  1. package/README.md +20 -168
  2. package/dist/esm/src/addresses.d.ts +20 -0
  3. package/dist/esm/src/addresses.d.ts.map +1 -0
  4. package/dist/esm/src/addresses.js +33 -0
  5. package/dist/esm/src/client/HsrTagIndex.d.ts +77 -0
  6. package/dist/esm/src/client/HsrTagIndex.d.ts.map +1 -0
  7. package/dist/esm/src/client/HsrTagIndex.js +157 -0
  8. package/dist/esm/src/client/PendingManager.d.ts +65 -0
  9. package/dist/esm/src/client/PendingManager.d.ts.map +1 -0
  10. package/dist/esm/src/client/PendingManager.js +84 -0
  11. package/dist/esm/src/client/SessionManager.d.ts +65 -0
  12. package/dist/esm/src/client/SessionManager.d.ts.map +1 -0
  13. package/dist/esm/src/client/SessionManager.js +146 -0
  14. package/dist/esm/src/client/VerbethClient.d.ts +153 -99
  15. package/dist/esm/src/client/VerbethClient.d.ts.map +1 -1
  16. package/dist/esm/src/client/VerbethClient.js +429 -123
  17. package/dist/esm/src/client/VerbethClientBuilder.d.ts +105 -0
  18. package/dist/esm/src/client/VerbethClientBuilder.d.ts.map +1 -0
  19. package/dist/esm/src/client/VerbethClientBuilder.js +146 -0
  20. package/dist/esm/src/client/hsrMatcher.d.ts +22 -0
  21. package/dist/esm/src/client/hsrMatcher.d.ts.map +1 -0
  22. package/dist/esm/src/client/hsrMatcher.js +31 -0
  23. package/dist/esm/src/client/index.d.ts +6 -1
  24. package/dist/esm/src/client/index.d.ts.map +1 -1
  25. package/dist/esm/src/client/index.js +2 -0
  26. package/dist/esm/src/client/types.d.ts +151 -10
  27. package/dist/esm/src/client/types.d.ts.map +1 -1
  28. package/dist/esm/src/crypto(old).d.ts +46 -0
  29. package/dist/esm/src/crypto(old).d.ts.map +1 -0
  30. package/dist/esm/src/crypto(old).js +137 -0
  31. package/dist/esm/src/crypto.d.ts +7 -29
  32. package/dist/esm/src/crypto.d.ts.map +1 -1
  33. package/dist/esm/src/crypto.js +36 -72
  34. package/dist/esm/src/executor.d.ts +17 -18
  35. package/dist/esm/src/executor.d.ts.map +1 -1
  36. package/dist/esm/src/executor.js +54 -70
  37. package/dist/esm/src/handshake.d.ts +51 -0
  38. package/dist/esm/src/handshake.d.ts.map +1 -0
  39. package/dist/esm/src/handshake.js +105 -0
  40. package/dist/esm/src/identity.d.ts +24 -18
  41. package/dist/esm/src/identity.d.ts.map +1 -1
  42. package/dist/esm/src/identity.js +126 -31
  43. package/dist/esm/src/index.d.ts +11 -7
  44. package/dist/esm/src/index.d.ts.map +1 -1
  45. package/dist/esm/src/index.js +10 -7
  46. package/dist/esm/src/payload.d.ts +3 -30
  47. package/dist/esm/src/payload.d.ts.map +1 -1
  48. package/dist/esm/src/payload.js +3 -77
  49. package/dist/esm/src/pq/kem.d.ts +33 -0
  50. package/dist/esm/src/pq/kem.d.ts.map +1 -0
  51. package/dist/esm/src/pq/kem.js +40 -0
  52. package/dist/esm/src/ratchet/auth.d.ts +34 -0
  53. package/dist/esm/src/ratchet/auth.d.ts.map +1 -0
  54. package/dist/esm/src/ratchet/auth.js +88 -0
  55. package/dist/esm/src/ratchet/codec.d.ts +52 -0
  56. package/dist/esm/src/ratchet/codec.d.ts.map +1 -0
  57. package/dist/esm/src/ratchet/codec.js +127 -0
  58. package/dist/esm/src/ratchet/decrypt.d.ts +28 -0
  59. package/dist/esm/src/ratchet/decrypt.d.ts.map +1 -0
  60. package/dist/esm/src/ratchet/decrypt.js +255 -0
  61. package/dist/esm/src/ratchet/encrypt.d.ts +17 -0
  62. package/dist/esm/src/ratchet/encrypt.d.ts.map +1 -0
  63. package/dist/esm/src/ratchet/encrypt.js +78 -0
  64. package/dist/esm/src/ratchet/index.d.ts +8 -0
  65. package/dist/esm/src/ratchet/index.d.ts.map +1 -0
  66. package/dist/esm/src/ratchet/index.js +8 -0
  67. package/dist/esm/src/ratchet/kdf.d.ts +60 -0
  68. package/dist/esm/src/ratchet/kdf.d.ts.map +1 -0
  69. package/dist/esm/src/ratchet/kdf.js +91 -0
  70. package/dist/esm/src/ratchet/session.d.ts +43 -0
  71. package/dist/esm/src/ratchet/session.d.ts.map +1 -0
  72. package/dist/esm/src/ratchet/session.js +139 -0
  73. package/dist/esm/src/ratchet/types.d.ts +168 -0
  74. package/dist/esm/src/ratchet/types.d.ts.map +1 -0
  75. package/dist/esm/src/ratchet/types.js +27 -0
  76. package/dist/esm/src/safeSessionSigner.d.ts +35 -0
  77. package/dist/esm/src/safeSessionSigner.d.ts.map +1 -0
  78. package/dist/esm/src/safeSessionSigner.js +59 -0
  79. package/dist/esm/src/send.d.ts +32 -24
  80. package/dist/esm/src/send.d.ts.map +1 -1
  81. package/dist/esm/src/send.js +84 -39
  82. package/dist/esm/src/types.d.ts +8 -13
  83. package/dist/esm/src/types.d.ts.map +1 -1
  84. package/dist/esm/src/utils/safeSessionSigner.d.ts +23 -0
  85. package/dist/esm/src/utils/safeSessionSigner.d.ts.map +1 -0
  86. package/dist/esm/src/utils/safeSessionSigner.js +59 -0
  87. package/dist/esm/src/utils/txQueue.d.ts +12 -0
  88. package/dist/esm/src/utils/txQueue.d.ts.map +1 -0
  89. package/dist/esm/src/utils/txQueue.js +25 -0
  90. package/dist/esm/src/utils.d.ts +2 -3
  91. package/dist/esm/src/utils.d.ts.map +1 -1
  92. package/dist/esm/src/utils.js +5 -5
  93. package/dist/esm/src/verify.d.ts +9 -25
  94. package/dist/esm/src/verify.d.ts.map +1 -1
  95. package/dist/esm/src/verify.js +49 -50
  96. package/dist/src/addresses.d.ts +20 -0
  97. package/dist/src/addresses.d.ts.map +1 -0
  98. package/dist/src/addresses.js +33 -0
  99. package/dist/src/client/HsrTagIndex.d.ts +77 -0
  100. package/dist/src/client/HsrTagIndex.d.ts.map +1 -0
  101. package/dist/src/client/HsrTagIndex.js +157 -0
  102. package/dist/src/client/PendingManager.d.ts +65 -0
  103. package/dist/src/client/PendingManager.d.ts.map +1 -0
  104. package/dist/src/client/PendingManager.js +84 -0
  105. package/dist/src/client/SessionManager.d.ts +65 -0
  106. package/dist/src/client/SessionManager.d.ts.map +1 -0
  107. package/dist/src/client/SessionManager.js +146 -0
  108. package/dist/src/client/VerbethClient.d.ts +153 -99
  109. package/dist/src/client/VerbethClient.d.ts.map +1 -1
  110. package/dist/src/client/VerbethClient.js +429 -123
  111. package/dist/src/client/VerbethClientBuilder.d.ts +105 -0
  112. package/dist/src/client/VerbethClientBuilder.d.ts.map +1 -0
  113. package/dist/src/client/VerbethClientBuilder.js +146 -0
  114. package/dist/src/client/hsrMatcher.d.ts +22 -0
  115. package/dist/src/client/hsrMatcher.d.ts.map +1 -0
  116. package/dist/src/client/hsrMatcher.js +31 -0
  117. package/dist/src/client/index.d.ts +6 -1
  118. package/dist/src/client/index.d.ts.map +1 -1
  119. package/dist/src/client/index.js +2 -0
  120. package/dist/src/client/types.d.ts +151 -10
  121. package/dist/src/client/types.d.ts.map +1 -1
  122. package/dist/src/crypto(old).d.ts +46 -0
  123. package/dist/src/crypto(old).d.ts.map +1 -0
  124. package/dist/src/crypto(old).js +137 -0
  125. package/dist/src/crypto.d.ts +7 -29
  126. package/dist/src/crypto.d.ts.map +1 -1
  127. package/dist/src/crypto.js +36 -72
  128. package/dist/src/executor.d.ts +17 -18
  129. package/dist/src/executor.d.ts.map +1 -1
  130. package/dist/src/executor.js +54 -70
  131. package/dist/src/handshake.d.ts +51 -0
  132. package/dist/src/handshake.d.ts.map +1 -0
  133. package/dist/src/handshake.js +105 -0
  134. package/dist/src/identity.d.ts +24 -18
  135. package/dist/src/identity.d.ts.map +1 -1
  136. package/dist/src/identity.js +126 -31
  137. package/dist/src/index.d.ts +11 -7
  138. package/dist/src/index.d.ts.map +1 -1
  139. package/dist/src/index.js +10 -7
  140. package/dist/src/payload.d.ts +3 -30
  141. package/dist/src/payload.d.ts.map +1 -1
  142. package/dist/src/payload.js +3 -77
  143. package/dist/src/pq/kem.d.ts +33 -0
  144. package/dist/src/pq/kem.d.ts.map +1 -0
  145. package/dist/src/pq/kem.js +40 -0
  146. package/dist/src/ratchet/auth.d.ts +34 -0
  147. package/dist/src/ratchet/auth.d.ts.map +1 -0
  148. package/dist/src/ratchet/auth.js +88 -0
  149. package/dist/src/ratchet/codec.d.ts +52 -0
  150. package/dist/src/ratchet/codec.d.ts.map +1 -0
  151. package/dist/src/ratchet/codec.js +127 -0
  152. package/dist/src/ratchet/decrypt.d.ts +28 -0
  153. package/dist/src/ratchet/decrypt.d.ts.map +1 -0
  154. package/dist/src/ratchet/decrypt.js +255 -0
  155. package/dist/src/ratchet/encrypt.d.ts +17 -0
  156. package/dist/src/ratchet/encrypt.d.ts.map +1 -0
  157. package/dist/src/ratchet/encrypt.js +78 -0
  158. package/dist/src/ratchet/index.d.ts +8 -0
  159. package/dist/src/ratchet/index.d.ts.map +1 -0
  160. package/dist/src/ratchet/index.js +8 -0
  161. package/dist/src/ratchet/kdf.d.ts +60 -0
  162. package/dist/src/ratchet/kdf.d.ts.map +1 -0
  163. package/dist/src/ratchet/kdf.js +91 -0
  164. package/dist/src/ratchet/session.d.ts +43 -0
  165. package/dist/src/ratchet/session.d.ts.map +1 -0
  166. package/dist/src/ratchet/session.js +139 -0
  167. package/dist/src/ratchet/types.d.ts +168 -0
  168. package/dist/src/ratchet/types.d.ts.map +1 -0
  169. package/dist/src/ratchet/types.js +27 -0
  170. package/dist/src/safeSessionSigner.d.ts +35 -0
  171. package/dist/src/safeSessionSigner.d.ts.map +1 -0
  172. package/dist/src/safeSessionSigner.js +59 -0
  173. package/dist/src/send.d.ts +32 -24
  174. package/dist/src/send.d.ts.map +1 -1
  175. package/dist/src/send.js +84 -39
  176. package/dist/src/types.d.ts +8 -13
  177. package/dist/src/types.d.ts.map +1 -1
  178. package/dist/src/utils/safeSessionSigner.d.ts +23 -0
  179. package/dist/src/utils/safeSessionSigner.d.ts.map +1 -0
  180. package/dist/src/utils/safeSessionSigner.js +59 -0
  181. package/dist/src/utils/txQueue.d.ts +12 -0
  182. package/dist/src/utils/txQueue.d.ts.map +1 -0
  183. package/dist/src/utils/txQueue.js +25 -0
  184. package/dist/src/utils.d.ts +2 -3
  185. package/dist/src/utils.d.ts.map +1 -1
  186. package/dist/src/utils.js +5 -5
  187. package/dist/src/verify.d.ts +9 -25
  188. package/dist/src/verify.d.ts.map +1 -1
  189. package/dist/src/verify.js +49 -50
  190. package/package.json +2 -1
@@ -1,169 +1,456 @@
1
1
  // packages/sdk/src/client/VerbethClient.ts
2
- import nacl from 'tweetnacl';
3
- import { initiateHandshake, respondToHandshake, sendEncryptedMessage } from '../send.js';
4
- import { deriveDuplexTopics } from '../crypto.js';
2
+ /**
3
+ * High-level client for Verbeth E2EE messaging.
4
+ *
5
+ * Provides a unified API for:
6
+ * - Handshake operations (sendHandshake, acceptHandshake)
7
+ * - Session creation for both initiator and responder
8
+ * - Message encryption/decryption with session management
9
+ * - Two-phase commit for message sending
10
+ * - Transaction confirmation handling
11
+ */
12
+ import { hexlify, getBytes, keccak256 } from 'ethers';
13
+ import { hkdf } from '@noble/hashes/hkdf';
14
+ import { sha256 } from '@noble/hashes/sha2';
15
+ import { initiateHandshake, respondToHandshake } from '../handshake.js';
16
+ import { kem } from '../pq/kem.js';
5
17
  import * as crypto from '../crypto.js';
6
18
  import * as payload from '../payload.js';
7
19
  import * as verify from '../verify.js';
8
20
  import * as utils from '../utils.js';
9
21
  import * as identity from '../identity.js';
10
- /**
11
- * High-level client for Verbeth E2EE messaging
12
- *
13
- * VerbethClient provides a simplified API for common operations while
14
- * maintaining access to all low-level functions.
15
- *
16
- * @example
17
- * ```typescript
18
- * const client = new VerbethClient({
19
- * executor,
20
- * identityKeyPair,
21
- * identityProof,
22
- * signer,
23
- * address: '0x...'
24
- * });
25
- *
26
- * // Send a handshake
27
- * const { tx, ephemeralKeyPair } = await client.sendHandshake(
28
- * '0xBob...',
29
- * 'Hello Bob!'
30
- * );
31
- *
32
- * // Send a message
33
- * await client.sendMessage(
34
- * contact.topicOutbound,
35
- * contact.identityPubKey,
36
- * 'Hello again!'
37
- * );
38
- * ```
39
- */
22
+ import * as ratchet from '../ratchet/index.js';
23
+ import { ratchetEncrypt } from '../ratchet/encrypt.js';
24
+ import { ratchetDecrypt } from '../ratchet/decrypt.js';
25
+ import { packageRatchetPayload, parseRatchetPayload, isRatchetPayload } from '../ratchet/codec.js';
26
+ import { verifyMessageSignature } from '../ratchet/auth.js';
27
+ import { dh, hybridInitialSecret } from '../ratchet/kdf.js';
28
+ import { initSessionAsInitiator, initSessionAsResponder } from '../ratchet/session.js';
29
+ import { SessionManager } from './SessionManager.js';
30
+ import { PendingManager } from './PendingManager.js';
40
31
  export class VerbethClient {
41
- /**
42
- * creates a new VerbethClient instance
43
- *
44
- * @param config - Client configuration with session-level parameters
45
- */
46
32
  constructor(config) {
47
33
  this.executor = config.executor;
48
34
  this.identityKeyPair = config.identityKeyPair;
49
35
  this.identityProof = config.identityProof;
50
36
  this.signer = config.signer;
51
37
  this.address = config.address;
38
+ this.callbacks = config.callbacks;
39
+ }
40
+ /**
41
+ * to be called before using prepareMessage/decryptMessage/sendMessage.
42
+ */
43
+ setSessionStore(store) {
44
+ this.sessionManager = new SessionManager(store);
45
+ }
46
+ /**
47
+ * to be called before using sendMessage/confirmTx/revertTx.
48
+ */
49
+ setPendingStore(store) {
50
+ this.pendingManager = new PendingManager(store);
51
+ }
52
+ hasSessionStore() {
53
+ return !!this.sessionManager;
54
+ }
55
+ hasPendingStore() {
56
+ return !!this.pendingManager;
52
57
  }
53
58
  /**
54
- * Initiates a handshake with a recipient
59
+ * Initiates a handshake with a recipient.
55
60
  *
56
- * generates an ephemeral keypair for this handshake.
57
- * the ephemeralKeyPair must be stored to decrypt the response later.
61
+ * Generates an ephemeral keypair and ML-KEM keypair for this handshake.
62
+ * Both secretKeys must be stored for ratchet session initialization
63
+ * when the response arrives.
58
64
  *
59
65
  * @param recipientAddress - Blockchain address of the recipient
60
66
  * @param message - Plaintext message to include in the handshake
61
- * @returns Transaction response and the ephemeral keypair (must be stored!)
62
- *
63
- * @example
64
- * ```typescript
65
- * const { tx, ephemeralKeyPair } = await client.sendHandshake(
66
- * '0xBob...',
67
- * 'Hi Bob!'
68
- * );
69
- *
70
- * // Store ephemeralKeyPair.secretKey to decrypt Bob's response
71
- * await storage.saveContact({
72
- * address: '0xBob...',
73
- * ephemeralKey: ephemeralKeyPair.secretKey,
74
- * // ...
75
- * });
76
- * ```
67
+ * @returns Transaction response, ephemeral keypair, and KEM keypair
77
68
  */
78
69
  async sendHandshake(recipientAddress, message) {
79
- const ephemeralKeyPair = nacl.box.keyPair();
80
- const tx = await initiateHandshake({
70
+ const { tx, ephemeralKeyPair, kemKeyPair } = await initiateHandshake({
81
71
  executor: this.executor,
82
72
  recipientAddress,
83
73
  identityKeyPair: this.identityKeyPair,
84
- ephemeralPubKey: ephemeralKeyPair.publicKey,
85
74
  plaintextPayload: message,
86
75
  identityProof: this.identityProof,
87
76
  signer: this.signer,
88
77
  });
89
- return { tx, ephemeralKeyPair };
78
+ return { tx, ephemeralKeyPair, kemKeyPair };
90
79
  }
91
80
  /**
92
- * Accepts a handshake from an initiator
93
- *
94
- * derives duplex topics for the conversation and returns them.
95
- *
96
- * @param initiatorEphemeralPubKey - initiator's ephemeral public key from handshake event
97
- * @param initiatorIdentityPubKey - initiator's long-term X25519 identity key
98
- * @param note - response message to send back
99
- * @returns transaction, derived duplex topics, and response tag
100
- *
101
- * @example
102
- * ```typescript
103
- * const { tx, duplexTopics } = await client.acceptHandshake(
104
- * handshake.ephemeralPubKey,
105
- * handshake.identityPubKey,
106
- * 'Hello Alice!'
107
- * );
108
- *
109
- * // Store the topics for future messaging
110
- * await storage.saveContact({
111
- * address: handshake.sender,
112
- * topicOutbound: duplexTopics.topicIn, // Responder writes to topicIn
113
- * topicInbound: duplexTopics.topicOut, // Responder reads from topicOut
114
- * // ...
115
- * });
116
- * ```
81
+ * Accepts a handshake from an initiator.
82
+ *
83
+ * Derives topics from ephemeral DH shared secret (same approach
84
+ * as post-handshake topic ratcheting). Returns topicOutbound/topicInbound
85
+ * directly instead of duplexTopics structure.
86
+ *
87
+ * Supports PQ-hybrid: if initiator includes ML-KEM public key (1216 bytes),
88
+ * performs KEM encapsulation and returns kemSharedSecret.
89
+ *
90
+ * @param initiatorEphemeralPubKey - Initiator's ephemeral key (32 bytes X25519 or 1216 bytes with KEM)
91
+ * @param note - Response message to send back
92
+ * @returns Transaction, derived topics, ephemeral keys for ratchet, and KEM shared secret
117
93
  */
118
- async acceptHandshake(initiatorEphemeralPubKey, initiatorIdentityPubKey, note) {
119
- const { tx, salt, tag } = await respondToHandshake({
94
+ async acceptHandshake(initiatorEphemeralPubKey, note) {
95
+ const { tx, salt, tag, responderEphemeralSecret, responderEphemeralPublic, kemSharedSecret, } = await respondToHandshake({
120
96
  executor: this.executor,
121
- initiatorPubKey: initiatorEphemeralPubKey,
97
+ initiatorEphemeralPubKey,
122
98
  responderIdentityKeyPair: this.identityKeyPair,
123
99
  note,
124
100
  identityProof: this.identityProof,
125
101
  signer: this.signer,
126
- initiatorIdentityPubKey,
127
102
  });
128
- const duplexTopics = deriveDuplexTopics(this.identityKeyPair.secretKey, initiatorIdentityPubKey, salt);
129
- return { tx, duplexTopics, tag };
103
+ // Extract X25519 part for topic derivation (first 32 bytes if extended)
104
+ const x25519Pub = initiatorEphemeralPubKey.length > 32
105
+ ? initiatorEphemeralPubKey.slice(0, 32)
106
+ : initiatorEphemeralPubKey;
107
+ if (!kemSharedSecret) {
108
+ throw new Error("KEM is required for PQ-secure handshake");
109
+ }
110
+ const { topicOutbound, topicInbound } = this.deriveTopicsFromDH(responderEphemeralSecret, x25519Pub, salt, false, kemSharedSecret);
111
+ return {
112
+ tx,
113
+ topicOutbound,
114
+ topicInbound,
115
+ tag,
116
+ salt,
117
+ responderEphemeralSecret,
118
+ responderEphemeralPublic,
119
+ kemSharedSecret,
120
+ };
121
+ }
122
+ // ===========================================================================
123
+ // Session Creation - Encapsulates DH and topic derivation
124
+ // ===========================================================================
125
+ /**
126
+ * Create a ratchet session as the handshake initiator.
127
+ *
128
+ * Call this after receiving and validating a handshake response.
129
+ * Handles topic derivation from ephemeral DH internally.
130
+ *
131
+ * If KEM ciphertext and secret are provided (PQ-hybrid), decapsulates
132
+ * to derive hybrid shared secret for post-quantum security.
133
+ *
134
+ * @param params - Session creation parameters
135
+ * @returns Ready-to-save RatchetSession
136
+ */
137
+ createInitiatorSession(params) {
138
+ const { contactAddress, initiatorEphemeralSecret, responderEphemeralPubKey, inResponseToTag, kemCiphertext, initiatorKemSecret, } = params;
139
+ if (!kemCiphertext || !initiatorKemSecret) {
140
+ throw new Error("KEM is required for PQ-secure handshake");
141
+ }
142
+ const kemSecret = kem.decapsulate(kemCiphertext, initiatorKemSecret);
143
+ const salt = getBytes(inResponseToTag);
144
+ const { topicOutbound, topicInbound } = this.deriveTopicsFromDH(initiatorEphemeralSecret, responderEphemeralPubKey, salt, true, kemSecret);
145
+ return initSessionAsInitiator({
146
+ myAddress: this.address,
147
+ contactAddress,
148
+ myHandshakeEphemeralSecret: initiatorEphemeralSecret,
149
+ theirResponderEphemeralPubKey: responderEphemeralPubKey,
150
+ topicOutbound,
151
+ topicInbound,
152
+ kemSecret,
153
+ });
130
154
  }
131
155
  /**
132
- * Sends an encrypted message to a contact
133
- *
134
- * handles timestamp, signing keys, and sender address.
135
- *
136
- * @param topicOutbound - The outbound topic for this conversation
137
- * @param recipientPubKey - Recipient's X25519 public key (from handshake)
138
- * @param message - Plaintext message to encrypt and send
139
- * @returns Transaction response
140
- *
141
- * @example
142
- * ```typescript
143
- * await client.sendMessage(
144
- * contact.topicOutbound,
145
- * contact.identityPubKey,
146
- * 'Hello again!'
147
- * );
148
- * ```
156
+ * Create a ratchet session as the handshake responder.
157
+ *
158
+ * Call this after sending a handshake response.
159
+ * Handles topic derivation from ephemeral DH internally.
160
+ *
161
+ * If kemSharedSecret is provided (PQ-hybrid), uses hybrid KDF
162
+ * for post-quantum security.
163
+ *
164
+ * @param params - Session creation parameters
165
+ * @returns Ready-to-save RatchetSession
166
+ */
167
+ createResponderSession(params) {
168
+ const { contactAddress, responderEphemeralSecret, responderEphemeralPublic, initiatorEphemeralPubKey, salt, kemSharedSecret, } = params;
169
+ if (!kemSharedSecret) {
170
+ throw new Error("KEM is required for PQ-secure handshake");
171
+ }
172
+ // Extract X25519 part for topic derivation (first 32 bytes if extended)
173
+ const x25519Pub = initiatorEphemeralPubKey.length > 32
174
+ ? initiatorEphemeralPubKey.slice(0, 32)
175
+ : initiatorEphemeralPubKey;
176
+ const { topicOutbound, topicInbound } = this.deriveTopicsFromDH(responderEphemeralSecret, x25519Pub, salt, false, kemSharedSecret);
177
+ return initSessionAsResponder({
178
+ myAddress: this.address,
179
+ contactAddress,
180
+ myResponderEphemeralSecret: responderEphemeralSecret,
181
+ myResponderEphemeralPublic: responderEphemeralPublic,
182
+ theirHandshakeEphemeralPubKey: x25519Pub,
183
+ topicOutbound,
184
+ topicInbound,
185
+ kemSecret: kemSharedSecret,
186
+ });
187
+ }
188
+ /**
189
+ * Accepting a structured HSR event object instead of individual parameters scattered across variables.
149
190
  */
150
- async sendMessage(topicOutbound, recipientPubKey, message) {
151
- const signingKeyPair = {
152
- publicKey: this.identityKeyPair.signingPublicKey,
153
- secretKey: this.identityKeyPair.signingSecretKey,
191
+ createInitiatorSessionFromHsr(params) {
192
+ return this.createInitiatorSession({
193
+ contactAddress: params.contactAddress,
194
+ initiatorEphemeralSecret: params.myEphemeralSecret,
195
+ responderEphemeralPubKey: params.hsrEvent.responderEphemeralPubKey,
196
+ inResponseToTag: params.hsrEvent.inResponseToTag,
197
+ kemCiphertext: params.hsrEvent.kemCiphertext,
198
+ initiatorKemSecret: params.myKemSecret,
199
+ });
200
+ }
201
+ deriveTopicsFromDH(mySecret, theirPublic, salt, isInitiator, kemSecret) {
202
+ const ephemeralShared = dh(mySecret, theirPublic);
203
+ const hybridSecret = hybridInitialSecret(ephemeralShared, kemSecret);
204
+ const deriveEpoch0Topic = (direction) => {
205
+ const info = `verbeth:topic-${direction}:v2`;
206
+ const okm = hkdf(sha256, hybridSecret, salt, info, 32);
207
+ return keccak256(okm);
154
208
  };
155
- const timestamp = Math.floor(Date.now() / 1000);
156
- return sendEncryptedMessage({
157
- executor: this.executor,
158
- topic: topicOutbound,
159
- message,
160
- recipientPubKey,
161
- senderAddress: this.address,
162
- senderSignKeyPair: signingKeyPair,
163
- timestamp,
209
+ if (isInitiator) {
210
+ return {
211
+ topicOutbound: deriveEpoch0Topic('outbound'),
212
+ topicInbound: deriveEpoch0Topic('inbound'),
213
+ };
214
+ }
215
+ else {
216
+ return {
217
+ topicOutbound: deriveEpoch0Topic('inbound'),
218
+ topicInbound: deriveEpoch0Topic('outbound'),
219
+ };
220
+ }
221
+ }
222
+ // ===========================================================================
223
+ // Message Operations
224
+ // ===========================================================================
225
+ /**
226
+ * Prepare a message for sending (encrypt without submitting).
227
+ *
228
+ * Two-phase commit pattern:
229
+ * 1. prepareMessage() - encrypts and persists session state immediately
230
+ * 2. Submit transaction using prepared.payload and prepared.topic
231
+ * 3. On confirmation, call confirmTx() to clean up pending record
232
+ *
233
+ * Session state is committed immediately for forward secrecy.
234
+ * If tx fails, the ratchet slot is "burned" (receiver handles via skip keys).
235
+ *
236
+ * @param conversationId - The conversation to send in
237
+ * @param plaintext - Message text to encrypt
238
+ * @returns PreparedMessage with payload ready for on-chain submission
239
+ */
240
+ async prepareMessage(conversationId, plaintext) {
241
+ if (!this.sessionManager) {
242
+ throw new Error('SessionStore not configured. Call setSessionStore() first.');
243
+ }
244
+ const session = await this.sessionManager.getByConversationId(conversationId);
245
+ if (!session) {
246
+ throw new Error(`No session found for conversation: ${conversationId}`);
247
+ }
248
+ const plaintextBytes = new TextEncoder().encode(plaintext);
249
+ const encryptResult = ratchetEncrypt(session, plaintextBytes, this.identityKeyPair.signingSecretKey);
250
+ const packedPayload = packageRatchetPayload(encryptResult.signature, encryptResult.header, encryptResult.ciphertext);
251
+ await this.sessionManager.save(encryptResult.session);
252
+ const prepared = {
253
+ id: this.generatePreparedId(),
254
+ conversationId,
255
+ topic: encryptResult.topic,
256
+ payload: packedPayload,
257
+ plaintext,
258
+ sessionBefore: session,
259
+ sessionAfter: encryptResult.session,
260
+ messageNumber: session.sendingMsgNumber,
261
+ createdAt: Date.now(),
262
+ };
263
+ return prepared;
264
+ }
265
+ // Session already saved in prepareMessage for forward secrecy.
266
+ // So this method can be used for additional bookkeeping if needed.
267
+ async commitMessage(_prepared) {
268
+ }
269
+ /**
270
+ * Decrypt an incoming message.
271
+ *
272
+ * Handles:
273
+ * - Topic routing (current, next, previous)
274
+ * - Signature verification (DoS protection)
275
+ * - Ratchet decryption
276
+ * - Session state updates
277
+ * - Automatic topic promotion
278
+ *
279
+ * @param topic - The topic the message arrived on
280
+ * @param payload - Raw message payload (Uint8Array)
281
+ * @param senderSigningKey - Sender's Ed25519 signing public key
282
+ * @param isOwnMessage - Whether this is our own outbound message (echo)
283
+ * @returns DecryptedMessage or null if decryption fails
284
+ */
285
+ async decryptMessage(topic, payload, senderSigningKey, isOwnMessage = false) {
286
+ if (!this.sessionManager) {
287
+ throw new Error('SessionStore not configured. Call setSessionStore() first.');
288
+ }
289
+ if (isOwnMessage) {
290
+ return null;
291
+ }
292
+ const result = await this.sessionManager.getByInboundTopic(topic);
293
+ if (!result) {
294
+ return null;
295
+ }
296
+ const { session, topicMatch } = result;
297
+ if (!isRatchetPayload(payload)) {
298
+ return null;
299
+ }
300
+ const parsed = parseRatchetPayload(payload);
301
+ if (!parsed) {
302
+ return null;
303
+ }
304
+ const sigValid = verifyMessageSignature(parsed.signature, parsed.header, parsed.ciphertext, senderSigningKey);
305
+ if (!sigValid) {
306
+ return null;
307
+ }
308
+ const decryptResult = ratchetDecrypt(session, parsed.header, parsed.ciphertext);
309
+ if (!decryptResult) {
310
+ return null;
311
+ }
312
+ // Check for topic ratchet before saving
313
+ const topicRatcheted = decryptResult.session.topicEpoch > session.topicEpoch;
314
+ const previousTopicInbound = topicRatcheted ? session.currentTopicInbound : null;
315
+ await this.sessionManager.save(decryptResult.session);
316
+ // Invoke callbacks if configured
317
+ if (this.callbacks) {
318
+ if (topicRatcheted && this.callbacks.onTopicRatchet) {
319
+ this.callbacks.onTopicRatchet({
320
+ conversationId: session.conversationId,
321
+ previousTopicInbound,
322
+ currentTopicInbound: decryptResult.session.currentTopicInbound,
323
+ topicEpoch: decryptResult.session.topicEpoch,
324
+ });
325
+ }
326
+ if (this.callbacks.onMessageDecrypted) {
327
+ this.callbacks.onMessageDecrypted({
328
+ conversationId: session.conversationId,
329
+ topicMatch,
330
+ topicEpoch: decryptResult.session.topicEpoch,
331
+ });
332
+ }
333
+ }
334
+ const plaintextStr = new TextDecoder().decode(decryptResult.plaintext);
335
+ return {
336
+ conversationId: session.conversationId,
337
+ plaintext: plaintextStr,
338
+ isOwnMessage: false,
339
+ session: decryptResult.session,
340
+ topic,
341
+ topicMatch,
342
+ };
343
+ }
344
+ /**
345
+ * Send a message with full lifecycle management.
346
+ *
347
+ * This is the high-level API that handles:
348
+ * 1. Encryption (with session commit)
349
+ * 2. Pending record creation
350
+ * 3. Transaction submission
351
+ * 4. Status tracking
352
+ *
353
+ * After calling this, wait for on-chain confirmation and call confirmTx().
354
+ *
355
+ * @param conversationId - Conversation to send in
356
+ * @param plaintext - Message text
357
+ * @returns SendResult with txHash and metadata
358
+ */
359
+ async sendMessage(conversationId, plaintext) {
360
+ if (!this.sessionManager) {
361
+ throw new Error('SessionStore not configured. Call setSessionStore() first.');
362
+ }
363
+ if (!this.pendingManager) {
364
+ throw new Error('PendingStore not configured. Call setPendingStore() first.');
365
+ }
366
+ // 1. Prepare message (encrypts and persists session)
367
+ const prepared = await this.prepareMessage(conversationId, plaintext);
368
+ // 2. Create pending record
369
+ await this.pendingManager.create({
370
+ id: prepared.id,
371
+ conversationId,
372
+ topic: prepared.topic,
373
+ payloadHex: hexlify(prepared.payload),
374
+ plaintext,
375
+ sessionStateBefore: JSON.stringify(this.serializeSessionInfo(prepared.sessionBefore)),
376
+ sessionStateAfter: JSON.stringify(this.serializeSessionInfo(prepared.sessionAfter)),
377
+ createdAt: prepared.createdAt,
164
378
  });
379
+ // 3. Submit transaction
380
+ const timestamp = Math.floor(Date.now() / 1000);
381
+ const nonce = prepared.messageNumber;
382
+ try {
383
+ const tx = await this.executor.sendMessage(prepared.payload, prepared.topic, timestamp, BigInt(nonce));
384
+ // 4. Update pending with txHash
385
+ await this.pendingManager.markSubmitted(prepared.id, tx.hash);
386
+ return {
387
+ messageId: prepared.id,
388
+ txHash: tx.hash,
389
+ topic: prepared.topic,
390
+ messageNumber: nonce,
391
+ };
392
+ }
393
+ catch (error) {
394
+ // Mark as failed (ratchet slot is already burned)
395
+ await this.pendingManager.markFailed(prepared.id);
396
+ throw error;
397
+ }
398
+ }
399
+ /**
400
+ * Confirm a transaction after on-chain confirmation.
401
+ * Call this when you see your MessageSent event on-chain.
402
+ *
403
+ * @param txHash - Transaction hash to confirm
404
+ * @returns ConfirmResult or null if not found
405
+ */
406
+ async confirmTx(txHash) {
407
+ if (!this.pendingManager) {
408
+ throw new Error('PendingStore not configured.');
409
+ }
410
+ const pending = await this.pendingManager.getByTxHash(txHash);
411
+ if (!pending || pending.status !== 'submitted') {
412
+ return null;
413
+ }
414
+ // Finalize (delete pending record)
415
+ const finalized = await this.pendingManager.finalize(pending.id);
416
+ if (!finalized) {
417
+ return null;
418
+ }
419
+ return {
420
+ conversationId: finalized.conversationId,
421
+ plaintext: finalized.plaintext,
422
+ messageId: finalized.id,
423
+ };
424
+ }
425
+ /**
426
+ * Handle transaction failure/revert.
427
+ *
428
+ * The ratchet slot is already burned (session was persisted in prepareMessage).
429
+ * This just cleans up the pending record.
430
+ *
431
+ * @param txHash - Transaction hash that failed
432
+ */
433
+ async revertTx(txHash) {
434
+ if (!this.pendingManager) {
435
+ throw new Error('PendingStore not configured.');
436
+ }
437
+ const pending = await this.pendingManager.getByTxHash(txHash);
438
+ if (pending) {
439
+ await this.pendingManager.delete(pending.id);
440
+ }
441
+ }
442
+ invalidateSessionCache(conversationId) {
443
+ this.sessionManager?.invalidate(conversationId);
444
+ }
445
+ clearSessionCache() {
446
+ this.sessionManager?.clearCache();
447
+ }
448
+ async getSession(conversationId) {
449
+ return this.sessionManager?.getByConversationId(conversationId) ?? null;
165
450
  }
166
- // ========== low-level API ==========
451
+ // ===========================================================================
452
+ // Low-level API Access
453
+ // ===========================================================================
167
454
  get crypto() {
168
455
  return crypto;
169
456
  }
@@ -179,6 +466,9 @@ export class VerbethClient {
179
466
  get identity() {
180
467
  return identity;
181
468
  }
469
+ get ratchet() {
470
+ return ratchet;
471
+ }
182
472
  get executorInstance() {
183
473
  return this.executor;
184
474
  }
@@ -188,4 +478,20 @@ export class VerbethClient {
188
478
  get userAddress() {
189
479
  return this.address;
190
480
  }
481
+ get identityProofInstance() {
482
+ return this.identityProof;
483
+ }
484
+ generatePreparedId() {
485
+ return `prep-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
486
+ }
487
+ serializeSessionInfo(session) {
488
+ return {
489
+ conversationId: session.conversationId,
490
+ topicEpoch: session.topicEpoch,
491
+ sendingMsgNumber: session.sendingMsgNumber,
492
+ receivingMsgNumber: session.receivingMsgNumber,
493
+ currentTopicOutbound: session.currentTopicOutbound,
494
+ currentTopicInbound: session.currentTopicInbound,
495
+ };
496
+ }
191
497
  }