@verbeth/sdk 0.1.4 → 0.1.5

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 (184) hide show
  1. package/README.md +16 -167
  2. package/dist/esm/src/client/HsrTagIndex.d.ts +77 -0
  3. package/dist/esm/src/client/HsrTagIndex.d.ts.map +1 -0
  4. package/dist/esm/src/client/HsrTagIndex.js +157 -0
  5. package/dist/esm/src/client/PendingManager.d.ts +65 -0
  6. package/dist/esm/src/client/PendingManager.d.ts.map +1 -0
  7. package/dist/esm/src/client/PendingManager.js +84 -0
  8. package/dist/esm/src/client/SessionManager.d.ts +65 -0
  9. package/dist/esm/src/client/SessionManager.d.ts.map +1 -0
  10. package/dist/esm/src/client/SessionManager.js +146 -0
  11. package/dist/esm/src/client/VerbethClient.d.ts +153 -99
  12. package/dist/esm/src/client/VerbethClient.d.ts.map +1 -1
  13. package/dist/esm/src/client/VerbethClient.js +429 -123
  14. package/dist/esm/src/client/VerbethClientBuilder.d.ts +105 -0
  15. package/dist/esm/src/client/VerbethClientBuilder.d.ts.map +1 -0
  16. package/dist/esm/src/client/VerbethClientBuilder.js +146 -0
  17. package/dist/esm/src/client/hsrMatcher.d.ts +22 -0
  18. package/dist/esm/src/client/hsrMatcher.d.ts.map +1 -0
  19. package/dist/esm/src/client/hsrMatcher.js +31 -0
  20. package/dist/esm/src/client/index.d.ts +6 -1
  21. package/dist/esm/src/client/index.d.ts.map +1 -1
  22. package/dist/esm/src/client/index.js +2 -0
  23. package/dist/esm/src/client/types.d.ts +151 -10
  24. package/dist/esm/src/client/types.d.ts.map +1 -1
  25. package/dist/esm/src/crypto(old).d.ts +46 -0
  26. package/dist/esm/src/crypto(old).d.ts.map +1 -0
  27. package/dist/esm/src/crypto(old).js +137 -0
  28. package/dist/esm/src/crypto.d.ts +7 -29
  29. package/dist/esm/src/crypto.d.ts.map +1 -1
  30. package/dist/esm/src/crypto.js +36 -72
  31. package/dist/esm/src/executor.d.ts +1 -2
  32. package/dist/esm/src/executor.d.ts.map +1 -1
  33. package/dist/esm/src/executor.js +8 -24
  34. package/dist/esm/src/handshake.d.ts +51 -0
  35. package/dist/esm/src/handshake.d.ts.map +1 -0
  36. package/dist/esm/src/handshake.js +105 -0
  37. package/dist/esm/src/identity.d.ts +24 -18
  38. package/dist/esm/src/identity.d.ts.map +1 -1
  39. package/dist/esm/src/identity.js +126 -31
  40. package/dist/esm/src/index.d.ts +10 -7
  41. package/dist/esm/src/index.d.ts.map +1 -1
  42. package/dist/esm/src/index.js +9 -7
  43. package/dist/esm/src/payload.d.ts +3 -30
  44. package/dist/esm/src/payload.d.ts.map +1 -1
  45. package/dist/esm/src/payload.js +3 -77
  46. package/dist/esm/src/pq/kem.d.ts +33 -0
  47. package/dist/esm/src/pq/kem.d.ts.map +1 -0
  48. package/dist/esm/src/pq/kem.js +40 -0
  49. package/dist/esm/src/ratchet/auth.d.ts +34 -0
  50. package/dist/esm/src/ratchet/auth.d.ts.map +1 -0
  51. package/dist/esm/src/ratchet/auth.js +88 -0
  52. package/dist/esm/src/ratchet/codec.d.ts +52 -0
  53. package/dist/esm/src/ratchet/codec.d.ts.map +1 -0
  54. package/dist/esm/src/ratchet/codec.js +127 -0
  55. package/dist/esm/src/ratchet/decrypt.d.ts +28 -0
  56. package/dist/esm/src/ratchet/decrypt.d.ts.map +1 -0
  57. package/dist/esm/src/ratchet/decrypt.js +255 -0
  58. package/dist/esm/src/ratchet/encrypt.d.ts +17 -0
  59. package/dist/esm/src/ratchet/encrypt.d.ts.map +1 -0
  60. package/dist/esm/src/ratchet/encrypt.js +78 -0
  61. package/dist/esm/src/ratchet/index.d.ts +8 -0
  62. package/dist/esm/src/ratchet/index.d.ts.map +1 -0
  63. package/dist/esm/src/ratchet/index.js +8 -0
  64. package/dist/esm/src/ratchet/kdf.d.ts +60 -0
  65. package/dist/esm/src/ratchet/kdf.d.ts.map +1 -0
  66. package/dist/esm/src/ratchet/kdf.js +91 -0
  67. package/dist/esm/src/ratchet/session.d.ts +43 -0
  68. package/dist/esm/src/ratchet/session.d.ts.map +1 -0
  69. package/dist/esm/src/ratchet/session.js +139 -0
  70. package/dist/esm/src/ratchet/types.d.ts +168 -0
  71. package/dist/esm/src/ratchet/types.d.ts.map +1 -0
  72. package/dist/esm/src/ratchet/types.js +27 -0
  73. package/dist/esm/src/safeSessionSigner.d.ts +35 -0
  74. package/dist/esm/src/safeSessionSigner.d.ts.map +1 -0
  75. package/dist/esm/src/safeSessionSigner.js +59 -0
  76. package/dist/esm/src/send.d.ts +32 -24
  77. package/dist/esm/src/send.d.ts.map +1 -1
  78. package/dist/esm/src/send.js +84 -39
  79. package/dist/esm/src/types.d.ts +8 -13
  80. package/dist/esm/src/types.d.ts.map +1 -1
  81. package/dist/esm/src/utils/safeSessionSigner.d.ts +23 -0
  82. package/dist/esm/src/utils/safeSessionSigner.d.ts.map +1 -0
  83. package/dist/esm/src/utils/safeSessionSigner.js +59 -0
  84. package/dist/esm/src/utils/txQueue.d.ts +12 -0
  85. package/dist/esm/src/utils/txQueue.d.ts.map +1 -0
  86. package/dist/esm/src/utils/txQueue.js +25 -0
  87. package/dist/esm/src/utils.d.ts +2 -3
  88. package/dist/esm/src/utils.d.ts.map +1 -1
  89. package/dist/esm/src/utils.js +5 -5
  90. package/dist/esm/src/verify.d.ts +9 -25
  91. package/dist/esm/src/verify.d.ts.map +1 -1
  92. package/dist/esm/src/verify.js +49 -50
  93. package/dist/src/client/HsrTagIndex.d.ts +77 -0
  94. package/dist/src/client/HsrTagIndex.d.ts.map +1 -0
  95. package/dist/src/client/HsrTagIndex.js +157 -0
  96. package/dist/src/client/PendingManager.d.ts +65 -0
  97. package/dist/src/client/PendingManager.d.ts.map +1 -0
  98. package/dist/src/client/PendingManager.js +84 -0
  99. package/dist/src/client/SessionManager.d.ts +65 -0
  100. package/dist/src/client/SessionManager.d.ts.map +1 -0
  101. package/dist/src/client/SessionManager.js +146 -0
  102. package/dist/src/client/VerbethClient.d.ts +153 -99
  103. package/dist/src/client/VerbethClient.d.ts.map +1 -1
  104. package/dist/src/client/VerbethClient.js +429 -123
  105. package/dist/src/client/VerbethClientBuilder.d.ts +105 -0
  106. package/dist/src/client/VerbethClientBuilder.d.ts.map +1 -0
  107. package/dist/src/client/VerbethClientBuilder.js +146 -0
  108. package/dist/src/client/hsrMatcher.d.ts +22 -0
  109. package/dist/src/client/hsrMatcher.d.ts.map +1 -0
  110. package/dist/src/client/hsrMatcher.js +31 -0
  111. package/dist/src/client/index.d.ts +6 -1
  112. package/dist/src/client/index.d.ts.map +1 -1
  113. package/dist/src/client/index.js +2 -0
  114. package/dist/src/client/types.d.ts +151 -10
  115. package/dist/src/client/types.d.ts.map +1 -1
  116. package/dist/src/crypto(old).d.ts +46 -0
  117. package/dist/src/crypto(old).d.ts.map +1 -0
  118. package/dist/src/crypto(old).js +137 -0
  119. package/dist/src/crypto.d.ts +7 -29
  120. package/dist/src/crypto.d.ts.map +1 -1
  121. package/dist/src/crypto.js +36 -72
  122. package/dist/src/executor.d.ts +1 -2
  123. package/dist/src/executor.d.ts.map +1 -1
  124. package/dist/src/executor.js +8 -24
  125. package/dist/src/handshake.d.ts +51 -0
  126. package/dist/src/handshake.d.ts.map +1 -0
  127. package/dist/src/handshake.js +105 -0
  128. package/dist/src/identity.d.ts +24 -18
  129. package/dist/src/identity.d.ts.map +1 -1
  130. package/dist/src/identity.js +126 -31
  131. package/dist/src/index.d.ts +10 -7
  132. package/dist/src/index.d.ts.map +1 -1
  133. package/dist/src/index.js +9 -7
  134. package/dist/src/payload.d.ts +3 -30
  135. package/dist/src/payload.d.ts.map +1 -1
  136. package/dist/src/payload.js +3 -77
  137. package/dist/src/pq/kem.d.ts +33 -0
  138. package/dist/src/pq/kem.d.ts.map +1 -0
  139. package/dist/src/pq/kem.js +40 -0
  140. package/dist/src/ratchet/auth.d.ts +34 -0
  141. package/dist/src/ratchet/auth.d.ts.map +1 -0
  142. package/dist/src/ratchet/auth.js +88 -0
  143. package/dist/src/ratchet/codec.d.ts +52 -0
  144. package/dist/src/ratchet/codec.d.ts.map +1 -0
  145. package/dist/src/ratchet/codec.js +127 -0
  146. package/dist/src/ratchet/decrypt.d.ts +28 -0
  147. package/dist/src/ratchet/decrypt.d.ts.map +1 -0
  148. package/dist/src/ratchet/decrypt.js +255 -0
  149. package/dist/src/ratchet/encrypt.d.ts +17 -0
  150. package/dist/src/ratchet/encrypt.d.ts.map +1 -0
  151. package/dist/src/ratchet/encrypt.js +78 -0
  152. package/dist/src/ratchet/index.d.ts +8 -0
  153. package/dist/src/ratchet/index.d.ts.map +1 -0
  154. package/dist/src/ratchet/index.js +8 -0
  155. package/dist/src/ratchet/kdf.d.ts +60 -0
  156. package/dist/src/ratchet/kdf.d.ts.map +1 -0
  157. package/dist/src/ratchet/kdf.js +91 -0
  158. package/dist/src/ratchet/session.d.ts +43 -0
  159. package/dist/src/ratchet/session.d.ts.map +1 -0
  160. package/dist/src/ratchet/session.js +139 -0
  161. package/dist/src/ratchet/types.d.ts +168 -0
  162. package/dist/src/ratchet/types.d.ts.map +1 -0
  163. package/dist/src/ratchet/types.js +27 -0
  164. package/dist/src/safeSessionSigner.d.ts +35 -0
  165. package/dist/src/safeSessionSigner.d.ts.map +1 -0
  166. package/dist/src/safeSessionSigner.js +59 -0
  167. package/dist/src/send.d.ts +32 -24
  168. package/dist/src/send.d.ts.map +1 -1
  169. package/dist/src/send.js +84 -39
  170. package/dist/src/types.d.ts +8 -13
  171. package/dist/src/types.d.ts.map +1 -1
  172. package/dist/src/utils/safeSessionSigner.d.ts +23 -0
  173. package/dist/src/utils/safeSessionSigner.d.ts.map +1 -0
  174. package/dist/src/utils/safeSessionSigner.js +59 -0
  175. package/dist/src/utils/txQueue.d.ts +12 -0
  176. package/dist/src/utils/txQueue.d.ts.map +1 -0
  177. package/dist/src/utils/txQueue.js +25 -0
  178. package/dist/src/utils.d.ts +2 -3
  179. package/dist/src/utils.d.ts.map +1 -1
  180. package/dist/src/utils.js +5 -5
  181. package/dist/src/verify.d.ts +9 -25
  182. package/dist/src/verify.d.ts.map +1 -1
  183. package/dist/src/verify.js +49 -50
  184. package/package.json +2 -1
package/README.md CHANGED
@@ -1,191 +1,40 @@
1
1
  # @verbeth/sdk
2
2
 
3
- Verbeth enables secure, E2EE messaging using Ethereum event logs as the only transport layer. No servers, no relays—just the blockchain.
4
-
5
- ## Features
6
-
7
- - **End-to-end encryption** using NaCl Box (X25519 + XSalsa20-Poly1305)
8
- - **Forward secrecy** with ephemeral keys per message
9
- - **Handshake protocol** for secure key exchange
10
- - **Privacy-focused** with minimal metadata via `recipientHash`
11
- - **EOA & Smart Account support** (ERC-1271/6492 compatible)
12
- - **Fully on-chain** - no centralized infrastructure
13
-
14
- ## Installation
3
+ End-to-end encrypted messaging over public EVM blockchains.
15
4
 
5
+ ### Install
16
6
  ```bash
17
- npm install @verbeth/sdk ethers tweetnacl
7
+ npm install @verbeth/sdk
18
8
  ```
19
9
 
20
- ## Quick Start
10
+ ### Quickstart
11
+ ```ts
12
+ import { createVerbethClient, deriveIdentityKeyPairWithProof, ExecutorFactory } from '@verbeth/sdk';
13
+ import { ethers } from 'ethers';
21
14
 
22
- ### 1. Initialize with VerbethClient (Recommended)
15
+ const LOGCHAIN = '0x62720f39d5Ec6501508bDe4D152c1E13Fd2F6707';
23
16
 
24
- ```typescript
25
- import { VerbethClient, ExecutorFactory, deriveIdentityKeyPairWithProof } from '@verbeth/sdk';
26
- import { Contract, BrowserProvider } from 'ethers';
27
- import { LogChainV1__factory } from '@verbeth/contracts/typechain-types';
28
-
29
- // Setup
30
- const provider = new BrowserProvider(window.ethereum);
17
+ const provider = new ethers.BrowserProvider(window.ethereum);
31
18
  const signer = await provider.getSigner();
32
19
  const address = await signer.getAddress();
33
20
 
34
- // Create contract instance
35
- const contract = LogChainV1__factory.connect(LOGCHAIN_ADDRESS, signer);
36
-
37
- // Derive identity keys (done once, then stored)
38
- const { identityKeyPair, identityProof } = await deriveIdentityKeyPairWithProof(signer);
21
+ const { identityKeyPair, identityProof } = await deriveIdentityKeyPairWithProof(signer, address);
39
22
 
40
- // Create executor (handles transaction submission)
41
- const executor = ExecutorFactory.createEOA(contract);
42
-
43
- // Initialize client
44
- const client = new VerbethClient({
45
- executor,
46
- identityKeyPair,
47
- identityProof,
23
+ const contract = new ethers.Contract(LOGCHAIN, LogChainABI, signer);
24
+ const client = createVerbethClient({
25
+ address,
48
26
  signer,
49
- address
50
- });
51
-
52
- // Send a handshake to start chatting
53
- const { tx, ephemeralKeyPair } = await client.sendHandshake(
54
- '0xRecipientAddress...',
55
- 'Hello! Want to chat?'
56
- );
57
-
58
- // Store ephemeralKeyPair. you'll just need it to decrypt the handshake response!
59
-
60
- // Accept a handshake
61
- const { tx, duplexTopics } = await client.acceptHandshake(
62
- handshakeEvent.ephemeralPubKey,
63
- handshakeEvent.identityPubKey,
64
- 'Sure, lets chat!'
65
- );
66
-
67
- // Send encrypted messages
68
- await client.sendMessage(
69
- duplexTopics.topicOut,
70
- recipientIdentityPubKey,
71
- 'This message is encrypted!'
72
- );
73
-
74
- // Decrypt received messages
75
- const decrypted = await client.decryptMessage(
76
- messageEvent.ciphertext,
77
- senderIdentityPubKey
78
- );
79
- ```
80
-
81
- ### 2. Low-level API
82
-
83
- For more control, use the low-level functions:
84
-
85
- ```typescript
86
- import {
87
- initiateHandshake,
88
- respondToHandshake,
89
- sendEncryptedMessage,
90
- decryptMessage,
91
- deriveIdentityKeyPairWithProof
92
- } from '@verbeth/sdk';
93
-
94
- // Generate identity keys
95
- const { identityKeyPair, identityProof } = await deriveIdentityKeyPairWithProof(signer);
96
-
97
- // Initiate handshake
98
- const ephemeralKeyPair = nacl.box.keyPair();
99
- const tx = await initiateHandshake({
100
- executor,
101
- recipientAddress: '0xBob...',
102
- ephemeralPubKey: ephemeralKeyPair.publicKey,
103
27
  identityKeyPair,
104
28
  identityProof,
105
- plaintextPayload: 'Hi Bob!'
106
- });
107
-
108
- // Send encrypted message
109
- await sendEncryptedMessage({
110
- executor,
111
- topic: derivedTopic,
112
- message: 'Secret message',
113
- recipientPubKey: bobsIdentityKey,
114
- senderAddress: myAddress,
115
- senderSignKeyPair: identityKeyPair,
116
- timestamp: Date.now()
29
+ executor: ExecutorFactory.createEOA(contract),
117
30
  });
118
31
 
119
- // Decrypt message
120
- const plaintext = decryptMessage(
121
- ciphertext,
122
- senderIdentityPubKey,
123
- myIdentityKeyPair.secretKey
124
- );
32
+ await client.sendMessage(conversationId, 'Hello, encrypted world!');
125
33
  ```
126
34
 
127
- ## Smart Account Support
128
-
129
- Verbeth works with ERC-4337 smart accounts:
130
-
131
- ```typescript
132
- import { ExecutorFactory } from '@verbeth/sdk';
133
-
134
- // For UserOp-based execution
135
- const executor = ExecutorFactory.createUserOp(
136
- contract,
137
- bundler,
138
- smartAccount,
139
- signer
140
- );
141
-
142
- // For direct EntryPoint execution
143
- const executor = ExecutorFactory.createDirectEntryPoint(
144
- contract,
145
- entryPoint,
146
- smartAccountAddress,
147
- signer
148
- );
149
- ```
150
-
151
- ## Contract Addresses
152
-
153
- **LogChainV1 Singleton:** `0x41a3eaC0d858028E9228d1E2092e6178fc81c4f0`
154
-
155
- **ERC1967Proxy:** `0x62720f39d5Ec6501508bDe4D152c1E13Fd2F6707`
156
-
157
- ## How It Works
158
-
159
- 1. **Identity Keys**: Each account derives long-term X25519 (encryption) + Ed25519 (signing) keys bound to their address via signature
160
- 2. **Handshake**: Alice sends her ephemeral key + identity proof to Bob via a `Handshake` event
161
- 3. **Response**: Bob verifies Alice's identity and responds with his keys + duplex topics
162
- 4. **Messaging**: Both parties derive shared topics and exchange encrypted messages via `MessageSent` events
163
- 5. **Decryption**: Recipients monitor their inbound topic and decrypt with their identity key
164
-
165
-
166
- ## Security Considerations
167
-
168
- - **Forward Secrecy**: Fresh ephemeral keys per message provide sender-side forward secrecy
169
- - **Identity Binding**: Addresses are cryptographically bound to long-term keys via signature
170
- - **Non-Repudiation**: Optional Ed25519 signatures prove message origin
171
- - **Privacy**: RecipientHash hides recipient identity; duplex topics separate communication channels
172
-
173
- ⚠️ **Note**: Current design provides sender-side forward secrecy. Recipient-side FS requires ephemeral↔ephemeral or session ratcheting (e.g., Double Ratchet).
174
-
175
- ## Built With
176
-
177
- - [TweetNaCl](https://tweetnacl.js.org/) - Encryption primitives
178
- - [Ethers v6](https://docs.ethers.org/v6/) - Ethereum interactions
179
- - [Viem](https://viem.sh/) - EIP-1271/6492 verification
180
- - [Noble Curves](https://github.com/paulmillr/noble-curves) - Elliptic curve operations
181
-
182
- ## Examples
183
-
184
- Check out the [demo application](https://github.com/okrame/verbeth-sdk/tree/main/apps/demo) for a complete implementation.
185
-
186
35
  ## Documentation
187
36
 
188
- For detailed protocol documentation, security analysis, and improvement proposals, see the [main repository](https://github.com/okrame/verbeth-sdk).
37
+ For detailed protocol documentation, see [docs.verbeth.xyz](https://docs.verbeth.xyz).
189
38
 
190
39
  ## License
191
40
 
@@ -0,0 +1,77 @@
1
+ export interface PendingContactEntry {
2
+ address: string;
3
+ handshakeEphemeralSecret: Uint8Array;
4
+ kemSecretKey: Uint8Array;
5
+ }
6
+ /**
7
+ * Index for efficient HSR tag matching.
8
+ *
9
+ * When an HSR arrives, we need to find which pending contact it belongs to.
10
+ * This requires computing tag = computeTagFromInitiator(secret, R) for each
11
+ * pending contact until we find a match.
12
+ *
13
+ * This class caches the computed tags per (contact, R) pair, making
14
+ * subsequent lookups O(1) after the first computation.
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * const hsrIndex = new HsrTagIndex();
19
+ *
20
+ * // When pending contacts change
21
+ * hsrIndex.rebuild(pendingContacts.map(c => ({
22
+ * address: c.address,
23
+ * handshakeEphemeralSecret: c.handshakeEphemeralSecret
24
+ * })));
25
+ *
26
+ * // Matching O(1) after first computation for each R
27
+ * const matchedAddress = hsrIndex.matchByTag(inResponseToTag, R);
28
+ * ```
29
+ */
30
+ export declare class HsrTagIndex {
31
+ private entries;
32
+ private tagToAddress;
33
+ /**
34
+ * Rebuild the index with a new set of pending contacts.
35
+ *
36
+ * Preserves cached tag computations for contacts that remain.
37
+ * Clears entries for contacts no longer in the list.
38
+ *
39
+ * @param contacts - Current pending contacts
40
+ */
41
+ rebuild(contacts: PendingContactEntry[]): void;
42
+ /**
43
+ * Add a single pending contact without full rebuild.
44
+ *
45
+ * @param contact - Contact to add
46
+ */
47
+ addContact(contact: PendingContactEntry): void;
48
+ /**
49
+ * Remove a contact from the index.
50
+ *
51
+ * @param address - Address of contact to remove
52
+ */
53
+ removeContact(address: string): void;
54
+ clear(): void;
55
+ /**
56
+ * Match an HSR by its tag using hybrid (PQ-secure) computation.
57
+ *
58
+ * Decrypts the payload internally to extract kemCiphertext, then
59
+ * decapsulates and computes the hybrid tag for matching.
60
+ *
61
+ * @param inResponseToTag - The tag from the HSR event
62
+ * @param R - Responder's ephemeral public key (from HSR event)
63
+ * @param encryptedPayload - JSON string of the encrypted HSR payload
64
+ * @returns Address of matching contact, or null if no match
65
+ */
66
+ matchByTag(inResponseToTag: `0x${string}`, R: Uint8Array, encryptedPayload: string): string | null;
67
+ /**
68
+ * Get the number of indexed contacts.
69
+ */
70
+ get size(): number;
71
+ /**
72
+ * Check if a contact is in the index.
73
+ */
74
+ hasContact(address: string): boolean;
75
+ private secretsEqual;
76
+ }
77
+ //# sourceMappingURL=HsrTagIndex.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HsrTagIndex.d.ts","sourceRoot":"","sources":["../../../../src/client/HsrTagIndex.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,wBAAwB,EAAE,UAAU,CAAC;IACrC,YAAY,EAAE,UAAU,CAAC;CAC1B;AAQD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,OAAO,CAAsC;IACrD,OAAO,CAAC,YAAY,CAAkC;IAEtD;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ,EAAE,mBAAmB,EAAE,GAAG,IAAI;IAuB9C;;;;OAIG;IACH,UAAU,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI;IAc9C;;;;OAIG;IACH,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAcpC,KAAK,IAAI,IAAI;IAKb;;;;;;;;;;OAUG;IACH,UAAU,CACR,eAAe,EAAE,KAAK,MAAM,EAAE,EAC9B,CAAC,EAAE,UAAU,EACb,gBAAgB,EAAE,MAAM,GACvB,MAAM,GAAG,IAAI;IAyBhB;;OAEG;IACH,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;OAEG;IACH,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAIpC,OAAO,CAAC,YAAY;CAOrB"}
@@ -0,0 +1,157 @@
1
+ // packages/sdk/src/client/HsrTagIndex.ts
2
+ /**
3
+ * Index for O(1) HSR (Handshake Response) matching.
4
+ *
5
+ * Caches computed tags for pending contacts to avoid O(n) loops
6
+ * when matching incoming handshake responses.
7
+ */
8
+ import { computeHybridTagFromInitiator, decryptHandshakeResponse } from '../crypto.js';
9
+ import { kem } from '../pq/kem.js';
10
+ /**
11
+ * Index for efficient HSR tag matching.
12
+ *
13
+ * When an HSR arrives, we need to find which pending contact it belongs to.
14
+ * This requires computing tag = computeTagFromInitiator(secret, R) for each
15
+ * pending contact until we find a match.
16
+ *
17
+ * This class caches the computed tags per (contact, R) pair, making
18
+ * subsequent lookups O(1) after the first computation.
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * const hsrIndex = new HsrTagIndex();
23
+ *
24
+ * // When pending contacts change
25
+ * hsrIndex.rebuild(pendingContacts.map(c => ({
26
+ * address: c.address,
27
+ * handshakeEphemeralSecret: c.handshakeEphemeralSecret
28
+ * })));
29
+ *
30
+ * // Matching O(1) after first computation for each R
31
+ * const matchedAddress = hsrIndex.matchByTag(inResponseToTag, R);
32
+ * ```
33
+ */
34
+ export class HsrTagIndex {
35
+ constructor() {
36
+ this.entries = new Map();
37
+ this.tagToAddress = new Map();
38
+ }
39
+ /**
40
+ * Rebuild the index with a new set of pending contacts.
41
+ *
42
+ * Preserves cached tag computations for contacts that remain.
43
+ * Clears entries for contacts no longer in the list.
44
+ *
45
+ * @param contacts - Current pending contacts
46
+ */
47
+ rebuild(contacts) {
48
+ const newEntries = new Map();
49
+ for (const contact of contacts) {
50
+ const existing = this.entries.get(contact.address);
51
+ if (existing &&
52
+ this.secretsEqual(existing.secret, contact.handshakeEphemeralSecret) &&
53
+ this.secretsEqual(existing.kemSecretKey, contact.kemSecretKey)) {
54
+ newEntries.set(contact.address, existing);
55
+ }
56
+ else {
57
+ newEntries.set(contact.address, {
58
+ address: contact.address,
59
+ secret: contact.handshakeEphemeralSecret,
60
+ kemSecretKey: contact.kemSecretKey,
61
+ });
62
+ }
63
+ }
64
+ // Keep tagToAddress cache - it's still valid for already computed tags
65
+ this.entries = newEntries;
66
+ }
67
+ /**
68
+ * Add a single pending contact without full rebuild.
69
+ *
70
+ * @param contact - Contact to add
71
+ */
72
+ addContact(contact) {
73
+ const existing = this.entries.get(contact.address);
74
+ if (!existing ||
75
+ !this.secretsEqual(existing.secret, contact.handshakeEphemeralSecret) ||
76
+ !this.secretsEqual(existing.kemSecretKey, contact.kemSecretKey)) {
77
+ this.entries.set(contact.address, {
78
+ address: contact.address,
79
+ secret: contact.handshakeEphemeralSecret,
80
+ kemSecretKey: contact.kemSecretKey,
81
+ });
82
+ }
83
+ }
84
+ /**
85
+ * Remove a contact from the index.
86
+ *
87
+ * @param address - Address of contact to remove
88
+ */
89
+ removeContact(address) {
90
+ const entry = this.entries.get(address);
91
+ if (entry) {
92
+ // Remove any cached tags for this address
93
+ for (const [tag, addr] of this.tagToAddress) {
94
+ if (addr === address) {
95
+ this.tagToAddress.delete(tag);
96
+ }
97
+ }
98
+ this.entries.delete(address);
99
+ }
100
+ }
101
+ clear() {
102
+ this.entries.clear();
103
+ this.tagToAddress.clear();
104
+ }
105
+ /**
106
+ * Match an HSR by its tag using hybrid (PQ-secure) computation.
107
+ *
108
+ * Decrypts the payload internally to extract kemCiphertext, then
109
+ * decapsulates and computes the hybrid tag for matching.
110
+ *
111
+ * @param inResponseToTag - The tag from the HSR event
112
+ * @param R - Responder's ephemeral public key (from HSR event)
113
+ * @param encryptedPayload - JSON string of the encrypted HSR payload
114
+ * @returns Address of matching contact, or null if no match
115
+ */
116
+ matchByTag(inResponseToTag, R, encryptedPayload) {
117
+ // Cache check
118
+ const cachedAddress = this.tagToAddress.get(inResponseToTag);
119
+ if (cachedAddress) {
120
+ return cachedAddress;
121
+ }
122
+ // For each contact: decrypt → extract kemCiphertext → decapsulate → compute hybrid tag
123
+ for (const [address, entry] of this.entries) {
124
+ const decrypted = decryptHandshakeResponse(encryptedPayload, entry.secret);
125
+ if (!decrypted || !decrypted.kemCiphertext)
126
+ continue;
127
+ const kemSecret = kem.decapsulate(decrypted.kemCiphertext, entry.kemSecretKey);
128
+ const tag = computeHybridTagFromInitiator(entry.secret, R, kemSecret);
129
+ this.tagToAddress.set(tag, address);
130
+ if (tag === inResponseToTag) {
131
+ return address;
132
+ }
133
+ }
134
+ return null;
135
+ }
136
+ /**
137
+ * Get the number of indexed contacts.
138
+ */
139
+ get size() {
140
+ return this.entries.size;
141
+ }
142
+ /**
143
+ * Check if a contact is in the index.
144
+ */
145
+ hasContact(address) {
146
+ return this.entries.has(address);
147
+ }
148
+ secretsEqual(a, b) {
149
+ if (a.length !== b.length)
150
+ return false;
151
+ for (let i = 0; i < a.length; i++) {
152
+ if (a[i] !== b[i])
153
+ return false;
154
+ }
155
+ return true;
156
+ }
157
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Internal Pending Message Coordinator.
3
+ *
4
+ * Manages the lifecycle of outbound messages:
5
+ * - Creating pending records before tx submission
6
+ * - Updating status on submission
7
+ * - Matching confirmations by txHash
8
+ * - Cleaning up after confirmation or failure
9
+ */
10
+ import { PendingStore, PendingMessage } from './types.js';
11
+ export interface CreatePendingParams {
12
+ id: string;
13
+ conversationId: string;
14
+ topic: string;
15
+ payloadHex: string;
16
+ plaintext: string;
17
+ sessionStateBefore: string;
18
+ sessionStateAfter: string;
19
+ createdAt: number;
20
+ }
21
+ export declare class PendingManager {
22
+ private store;
23
+ constructor(store: PendingStore);
24
+ /**
25
+ * Create and save a pending message record.
26
+ * Called right before submitting a transaction.
27
+ */
28
+ create(params: CreatePendingParams): Promise<PendingMessage>;
29
+ /**
30
+ * Mark as submitted with transaction hash.
31
+ * Called immediately after tx is broadcast.
32
+ */
33
+ markSubmitted(id: string, txHash: string): Promise<void>;
34
+ /**
35
+ * Mark as failed.
36
+ * Called when tx submission fails.
37
+ * Note: Ratchet slot is already burned (session was committed).
38
+ */
39
+ markFailed(id: string): Promise<void>;
40
+ get(id: string): Promise<PendingMessage | null>;
41
+ getByTxHash(txHash: string): Promise<PendingMessage | null>;
42
+ getByConversation(conversationId: string): Promise<PendingMessage[]>;
43
+ /**
44
+ * Finalize and delete.
45
+ * Called when we see our MessageSent event on-chain.
46
+ *
47
+ * @returns The finalized pending message, or null if not found
48
+ */
49
+ finalize(id: string): Promise<PendingMessage | null>;
50
+ /**
51
+ * Delete a pending message without finalizing.
52
+ * Used for cleanup on failure or cancellation.
53
+ */
54
+ delete(id: string): Promise<void>;
55
+ /**
56
+ * Clean up stale pending messages.
57
+ * Called periodically to remove old failed/stuck records.
58
+ *
59
+ * @param conversationId - Conversation to clean up
60
+ * @param maxAgeMs - Maximum age in milliseconds (default: 24 hours)
61
+ * @returns Number of records cleaned up
62
+ */
63
+ cleanupStale(conversationId: string, maxAgeMs?: number): Promise<number>;
64
+ }
65
+ //# sourceMappingURL=PendingManager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PendingManager.d.ts","sourceRoot":"","sources":["../../../../src/client/PendingManager.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AAEH,OAAO,EAAE,YAAY,EAAE,cAAc,EAAiB,MAAM,YAAY,CAAC;AAEzE,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,cAAc;IACb,OAAO,CAAC,KAAK;gBAAL,KAAK,EAAE,YAAY;IAEvC;;;OAGG;IACG,MAAM,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,cAAc,CAAC;IAUlE;;;OAGG;IACG,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9D;;;;OAIG;IACG,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKrC,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAK/C,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAI3D,iBAAiB,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAK1E;;;;;OAKG;IACG,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAU1D;;;OAGG;IACG,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvC;;;;;;;OAOG;IACG,YAAY,CAChB,cAAc,EAAE,MAAM,EACtB,QAAQ,GAAE,MAA4B,GACrC,OAAO,CAAC,MAAM,CAAC;CAcnB"}
@@ -0,0 +1,84 @@
1
+ // packages/sdk/src/client/PendingManager.ts
2
+ export class PendingManager {
3
+ constructor(store) {
4
+ this.store = store;
5
+ }
6
+ /**
7
+ * Create and save a pending message record.
8
+ * Called right before submitting a transaction.
9
+ */
10
+ async create(params) {
11
+ const pending = {
12
+ ...params,
13
+ txHash: null,
14
+ status: 'preparing',
15
+ };
16
+ await this.store.save(pending);
17
+ return pending;
18
+ }
19
+ /**
20
+ * Mark as submitted with transaction hash.
21
+ * Called immediately after tx is broadcast.
22
+ */
23
+ async markSubmitted(id, txHash) {
24
+ await this.store.updateStatus(id, 'submitted', txHash);
25
+ }
26
+ /**
27
+ * Mark as failed.
28
+ * Called when tx submission fails.
29
+ * Note: Ratchet slot is already burned (session was committed).
30
+ */
31
+ async markFailed(id) {
32
+ await this.store.updateStatus(id, 'failed');
33
+ }
34
+ async get(id) {
35
+ return this.store.get(id);
36
+ }
37
+ async getByTxHash(txHash) {
38
+ return this.store.getByTxHash(txHash);
39
+ }
40
+ async getByConversation(conversationId) {
41
+ return this.store.getByConversation(conversationId);
42
+ }
43
+ /**
44
+ * Finalize and delete.
45
+ * Called when we see our MessageSent event on-chain.
46
+ *
47
+ * @returns The finalized pending message, or null if not found
48
+ */
49
+ async finalize(id) {
50
+ const pending = await this.store.get(id);
51
+ if (!pending) {
52
+ return null;
53
+ }
54
+ await this.store.delete(id);
55
+ return pending;
56
+ }
57
+ /**
58
+ * Delete a pending message without finalizing.
59
+ * Used for cleanup on failure or cancellation.
60
+ */
61
+ async delete(id) {
62
+ await this.store.delete(id);
63
+ }
64
+ /**
65
+ * Clean up stale pending messages.
66
+ * Called periodically to remove old failed/stuck records.
67
+ *
68
+ * @param conversationId - Conversation to clean up
69
+ * @param maxAgeMs - Maximum age in milliseconds (default: 24 hours)
70
+ * @returns Number of records cleaned up
71
+ */
72
+ async cleanupStale(conversationId, maxAgeMs = 24 * 60 * 60 * 1000) {
73
+ const pending = await this.store.getByConversation(conversationId);
74
+ const cutoff = Date.now() - maxAgeMs;
75
+ let cleaned = 0;
76
+ for (const p of pending) {
77
+ if (p.createdAt < cutoff) {
78
+ await this.store.delete(p.id);
79
+ cleaned++;
80
+ }
81
+ }
82
+ return cleaned;
83
+ }
84
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Internal Session Coordinator.
3
+ *
4
+ * Handles:
5
+ * - Session caching for performance
6
+ * - Topic matching (current, next, previous)
7
+ * - Automatic topic promotion when next topic is used
8
+ * - Cache invalidation
9
+ */
10
+ import { RatchetSession } from '../ratchet/types.js';
11
+ import { SessionStore } from './types.js';
12
+ export interface TopicLookupResult {
13
+ session: RatchetSession;
14
+ topicMatch: 'current' | 'next' | 'previous';
15
+ }
16
+ /**
17
+ * Internal session manager that wraps a SessionStore with caching
18
+ * and topic promotion logic.
19
+ */
20
+ export declare class SessionManager {
21
+ private store;
22
+ private cache;
23
+ constructor(store: SessionStore);
24
+ /**
25
+ * Get session by conversation ID, checking cache first.
26
+ */
27
+ getByConversationId(conversationId: string): Promise<RatchetSession | null>;
28
+ /**
29
+ * Find session by inbound topic with automatic topic promotion.
30
+ *
31
+ * Checks topics in order:
32
+ * 1. currentTopicInbound - standard case
33
+ * 2. nextTopicInbound - DH ratchet advanced, promotes topics
34
+ * 3. previousTopicInbound - grace period for late messages
35
+ *
36
+ * @param topic - The topic to look up
37
+ * @returns Session and match type, or null if not found
38
+ */
39
+ getByInboundTopic(topic: string): Promise<TopicLookupResult | null>;
40
+ /**
41
+ * Update session in cache and persist to store.
42
+ */
43
+ save(session: RatchetSession): Promise<void>;
44
+ /**
45
+ * Update cache without persisting (for batch operations).
46
+ */
47
+ updateCache(session: RatchetSession): void;
48
+ /**
49
+ * Persist all cached sessions to store.
50
+ */
51
+ flushCache(): Promise<void>;
52
+ /**
53
+ * Invalidate cache entry (e.g., on session reset).
54
+ */
55
+ invalidate(conversationId: string): void;
56
+ clearCache(): void;
57
+ getCacheSize(): number;
58
+ isCached(conversationId: string): boolean;
59
+ /**
60
+ * Promote next topics to current (internal helper).
61
+ * Called when a message arrives on nextTopicInbound.
62
+ */
63
+ private promoteTopics;
64
+ }
65
+ //# sourceMappingURL=SessionManager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SessionManager.d.ts","sourceRoot":"","sources":["../../../../src/client/SessionManager.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AAEH,OAAO,EAAE,cAAc,EAA8B,MAAM,qBAAqB,CAAC;AACjF,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1C,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,cAAc,CAAC;IACxB,UAAU,EAAE,SAAS,GAAG,MAAM,GAAG,UAAU,CAAC;CAC7C;AAED;;;GAGG;AACH,qBAAa,cAAc;IAGb,OAAO,CAAC,KAAK;IAFzB,OAAO,CAAC,KAAK,CAAqC;gBAE9B,KAAK,EAAE,YAAY;IAMvC;;OAEG;IACG,mBAAmB,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAajF;;;;;;;;;;OAUG;IACG,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IA6CzE;;OAEG;IACG,IAAI,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAKlD;;OAEG;IACH,WAAW,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI;IAI1C;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IASjC;;OAEG;IACH,UAAU,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI;IAIxC,UAAU,IAAI,IAAI;IAIlB,YAAY,IAAI,MAAM;IAItB,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO;IAIzC;;;OAGG;IACH,OAAO,CAAC,aAAa;CAqBtB"}