@teneo-protocol/sdk 1.0.0

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 (281) hide show
  1. package/.dockerignore +14 -0
  2. package/.env.test.example +14 -0
  3. package/.eslintrc.json +26 -0
  4. package/.github/workflows/claude-code-review.yml +78 -0
  5. package/.github/workflows/claude-reviewer.yml +64 -0
  6. package/.github/workflows/publish-npm.yml +38 -0
  7. package/.github/workflows/push-to-main.yml +23 -0
  8. package/.node-version +1 -0
  9. package/.prettierrc +11 -0
  10. package/Dockerfile +25 -0
  11. package/LICENCE +661 -0
  12. package/README.md +709 -0
  13. package/dist/constants.d.ts +42 -0
  14. package/dist/constants.d.ts.map +1 -0
  15. package/dist/constants.js +45 -0
  16. package/dist/constants.js.map +1 -0
  17. package/dist/core/websocket-client.d.ts +261 -0
  18. package/dist/core/websocket-client.d.ts.map +1 -0
  19. package/dist/core/websocket-client.js +875 -0
  20. package/dist/core/websocket-client.js.map +1 -0
  21. package/dist/formatters/response-formatter.d.ts +354 -0
  22. package/dist/formatters/response-formatter.d.ts.map +1 -0
  23. package/dist/formatters/response-formatter.js +575 -0
  24. package/dist/formatters/response-formatter.js.map +1 -0
  25. package/dist/handlers/message-handler-registry.d.ts +155 -0
  26. package/dist/handlers/message-handler-registry.d.ts.map +1 -0
  27. package/dist/handlers/message-handler-registry.js +216 -0
  28. package/dist/handlers/message-handler-registry.js.map +1 -0
  29. package/dist/handlers/message-handlers/agent-selected-handler.d.ts +112 -0
  30. package/dist/handlers/message-handlers/agent-selected-handler.d.ts.map +1 -0
  31. package/dist/handlers/message-handlers/agent-selected-handler.js +40 -0
  32. package/dist/handlers/message-handlers/agent-selected-handler.js.map +1 -0
  33. package/dist/handlers/message-handlers/agents-list-handler.d.ts +14 -0
  34. package/dist/handlers/message-handlers/agents-list-handler.d.ts.map +1 -0
  35. package/dist/handlers/message-handlers/agents-list-handler.js +25 -0
  36. package/dist/handlers/message-handlers/agents-list-handler.js.map +1 -0
  37. package/dist/handlers/message-handlers/auth-error-handler.d.ts +71 -0
  38. package/dist/handlers/message-handlers/auth-error-handler.d.ts.map +1 -0
  39. package/dist/handlers/message-handlers/auth-error-handler.js +30 -0
  40. package/dist/handlers/message-handlers/auth-error-handler.js.map +1 -0
  41. package/dist/handlers/message-handlers/auth-message-handler.d.ts +18 -0
  42. package/dist/handlers/message-handlers/auth-message-handler.d.ts.map +1 -0
  43. package/dist/handlers/message-handlers/auth-message-handler.js +60 -0
  44. package/dist/handlers/message-handlers/auth-message-handler.js.map +1 -0
  45. package/dist/handlers/message-handlers/auth-required-handler.d.ts +76 -0
  46. package/dist/handlers/message-handlers/auth-required-handler.d.ts.map +1 -0
  47. package/dist/handlers/message-handlers/auth-required-handler.js +23 -0
  48. package/dist/handlers/message-handlers/auth-required-handler.js.map +1 -0
  49. package/dist/handlers/message-handlers/auth-success-handler.d.ts +18 -0
  50. package/dist/handlers/message-handlers/auth-success-handler.d.ts.map +1 -0
  51. package/dist/handlers/message-handlers/auth-success-handler.js +51 -0
  52. package/dist/handlers/message-handlers/auth-success-handler.js.map +1 -0
  53. package/dist/handlers/message-handlers/base-handler.d.ts +55 -0
  54. package/dist/handlers/message-handlers/base-handler.d.ts.map +1 -0
  55. package/dist/handlers/message-handlers/base-handler.js +83 -0
  56. package/dist/handlers/message-handlers/base-handler.js.map +1 -0
  57. package/dist/handlers/message-handlers/challenge-handler.d.ts +73 -0
  58. package/dist/handlers/message-handlers/challenge-handler.d.ts.map +1 -0
  59. package/dist/handlers/message-handlers/challenge-handler.js +47 -0
  60. package/dist/handlers/message-handlers/challenge-handler.js.map +1 -0
  61. package/dist/handlers/message-handlers/error-message-handler.d.ts +76 -0
  62. package/dist/handlers/message-handlers/error-message-handler.d.ts.map +1 -0
  63. package/dist/handlers/message-handlers/error-message-handler.js +29 -0
  64. package/dist/handlers/message-handlers/error-message-handler.js.map +1 -0
  65. package/dist/handlers/message-handlers/index.d.ts +28 -0
  66. package/dist/handlers/message-handlers/index.d.ts.map +1 -0
  67. package/dist/handlers/message-handlers/index.js +100 -0
  68. package/dist/handlers/message-handlers/index.js.map +1 -0
  69. package/dist/handlers/message-handlers/list-rooms-response-handler.d.ts +122 -0
  70. package/dist/handlers/message-handlers/list-rooms-response-handler.d.ts.map +1 -0
  71. package/dist/handlers/message-handlers/list-rooms-response-handler.js +30 -0
  72. package/dist/handlers/message-handlers/list-rooms-response-handler.js.map +1 -0
  73. package/dist/handlers/message-handlers/ping-pong-handler.d.ts +104 -0
  74. package/dist/handlers/message-handlers/ping-pong-handler.d.ts.map +1 -0
  75. package/dist/handlers/message-handlers/ping-pong-handler.js +36 -0
  76. package/dist/handlers/message-handlers/ping-pong-handler.js.map +1 -0
  77. package/dist/handlers/message-handlers/regular-message-handler.d.ts +56 -0
  78. package/dist/handlers/message-handlers/regular-message-handler.d.ts.map +1 -0
  79. package/dist/handlers/message-handlers/regular-message-handler.js +59 -0
  80. package/dist/handlers/message-handlers/regular-message-handler.js.map +1 -0
  81. package/dist/handlers/message-handlers/subscribe-response-handler.d.ts +81 -0
  82. package/dist/handlers/message-handlers/subscribe-response-handler.d.ts.map +1 -0
  83. package/dist/handlers/message-handlers/subscribe-response-handler.js +48 -0
  84. package/dist/handlers/message-handlers/subscribe-response-handler.js.map +1 -0
  85. package/dist/handlers/message-handlers/task-response-handler.d.ts +14 -0
  86. package/dist/handlers/message-handlers/task-response-handler.d.ts.map +1 -0
  87. package/dist/handlers/message-handlers/task-response-handler.js +44 -0
  88. package/dist/handlers/message-handlers/task-response-handler.js.map +1 -0
  89. package/dist/handlers/message-handlers/types.d.ts +51 -0
  90. package/dist/handlers/message-handlers/types.d.ts.map +1 -0
  91. package/dist/handlers/message-handlers/types.js +7 -0
  92. package/dist/handlers/message-handlers/types.js.map +1 -0
  93. package/dist/handlers/message-handlers/unsubscribe-response-handler.d.ts +81 -0
  94. package/dist/handlers/message-handlers/unsubscribe-response-handler.d.ts.map +1 -0
  95. package/dist/handlers/message-handlers/unsubscribe-response-handler.js +48 -0
  96. package/dist/handlers/message-handlers/unsubscribe-response-handler.js.map +1 -0
  97. package/dist/handlers/webhook-handler.d.ts +202 -0
  98. package/dist/handlers/webhook-handler.d.ts.map +1 -0
  99. package/dist/handlers/webhook-handler.js +511 -0
  100. package/dist/handlers/webhook-handler.js.map +1 -0
  101. package/dist/index.d.ts +71 -0
  102. package/dist/index.d.ts.map +1 -0
  103. package/dist/index.js +217 -0
  104. package/dist/index.js.map +1 -0
  105. package/dist/managers/agent-registry.d.ts +173 -0
  106. package/dist/managers/agent-registry.d.ts.map +1 -0
  107. package/dist/managers/agent-registry.js +310 -0
  108. package/dist/managers/agent-registry.js.map +1 -0
  109. package/dist/managers/connection-manager.d.ts +134 -0
  110. package/dist/managers/connection-manager.d.ts.map +1 -0
  111. package/dist/managers/connection-manager.js +176 -0
  112. package/dist/managers/connection-manager.js.map +1 -0
  113. package/dist/managers/index.d.ts +9 -0
  114. package/dist/managers/index.d.ts.map +1 -0
  115. package/dist/managers/index.js +16 -0
  116. package/dist/managers/index.js.map +1 -0
  117. package/dist/managers/message-router.d.ts +112 -0
  118. package/dist/managers/message-router.d.ts.map +1 -0
  119. package/dist/managers/message-router.js +260 -0
  120. package/dist/managers/message-router.js.map +1 -0
  121. package/dist/managers/room-manager.d.ts +165 -0
  122. package/dist/managers/room-manager.d.ts.map +1 -0
  123. package/dist/managers/room-manager.js +227 -0
  124. package/dist/managers/room-manager.js.map +1 -0
  125. package/dist/teneo-sdk.d.ts +703 -0
  126. package/dist/teneo-sdk.d.ts.map +1 -0
  127. package/dist/teneo-sdk.js +907 -0
  128. package/dist/teneo-sdk.js.map +1 -0
  129. package/dist/types/config.d.ts +1047 -0
  130. package/dist/types/config.d.ts.map +1 -0
  131. package/dist/types/config.js +720 -0
  132. package/dist/types/config.js.map +1 -0
  133. package/dist/types/error-codes.d.ts +29 -0
  134. package/dist/types/error-codes.d.ts.map +1 -0
  135. package/dist/types/error-codes.js +41 -0
  136. package/dist/types/error-codes.js.map +1 -0
  137. package/dist/types/events.d.ts +616 -0
  138. package/dist/types/events.d.ts.map +1 -0
  139. package/dist/types/events.js +261 -0
  140. package/dist/types/events.js.map +1 -0
  141. package/dist/types/health.d.ts +40 -0
  142. package/dist/types/health.d.ts.map +1 -0
  143. package/dist/types/health.js +6 -0
  144. package/dist/types/health.js.map +1 -0
  145. package/dist/types/index.d.ts +10 -0
  146. package/dist/types/index.d.ts.map +1 -0
  147. package/dist/types/index.js +123 -0
  148. package/dist/types/index.js.map +1 -0
  149. package/dist/types/messages.d.ts +3734 -0
  150. package/dist/types/messages.d.ts.map +1 -0
  151. package/dist/types/messages.js +482 -0
  152. package/dist/types/messages.js.map +1 -0
  153. package/dist/types/validation.d.ts +81 -0
  154. package/dist/types/validation.d.ts.map +1 -0
  155. package/dist/types/validation.js +115 -0
  156. package/dist/types/validation.js.map +1 -0
  157. package/dist/utils/bounded-queue.d.ts +127 -0
  158. package/dist/utils/bounded-queue.d.ts.map +1 -0
  159. package/dist/utils/bounded-queue.js +181 -0
  160. package/dist/utils/bounded-queue.js.map +1 -0
  161. package/dist/utils/circuit-breaker.d.ts +141 -0
  162. package/dist/utils/circuit-breaker.d.ts.map +1 -0
  163. package/dist/utils/circuit-breaker.js +215 -0
  164. package/dist/utils/circuit-breaker.js.map +1 -0
  165. package/dist/utils/deduplication-cache.d.ts +110 -0
  166. package/dist/utils/deduplication-cache.d.ts.map +1 -0
  167. package/dist/utils/deduplication-cache.js +177 -0
  168. package/dist/utils/deduplication-cache.js.map +1 -0
  169. package/dist/utils/event-waiter.d.ts +101 -0
  170. package/dist/utils/event-waiter.d.ts.map +1 -0
  171. package/dist/utils/event-waiter.js +118 -0
  172. package/dist/utils/event-waiter.js.map +1 -0
  173. package/dist/utils/index.d.ts +51 -0
  174. package/dist/utils/index.d.ts.map +1 -0
  175. package/dist/utils/index.js +72 -0
  176. package/dist/utils/index.js.map +1 -0
  177. package/dist/utils/logger.d.ts +22 -0
  178. package/dist/utils/logger.d.ts.map +1 -0
  179. package/dist/utils/logger.js +91 -0
  180. package/dist/utils/logger.js.map +1 -0
  181. package/dist/utils/rate-limiter.d.ts +122 -0
  182. package/dist/utils/rate-limiter.d.ts.map +1 -0
  183. package/dist/utils/rate-limiter.js +190 -0
  184. package/dist/utils/rate-limiter.js.map +1 -0
  185. package/dist/utils/retry-policy.d.ts +191 -0
  186. package/dist/utils/retry-policy.d.ts.map +1 -0
  187. package/dist/utils/retry-policy.js +225 -0
  188. package/dist/utils/retry-policy.js.map +1 -0
  189. package/dist/utils/secure-private-key.d.ts +113 -0
  190. package/dist/utils/secure-private-key.d.ts.map +1 -0
  191. package/dist/utils/secure-private-key.js +188 -0
  192. package/dist/utils/secure-private-key.js.map +1 -0
  193. package/dist/utils/signature-verifier.d.ts +143 -0
  194. package/dist/utils/signature-verifier.d.ts.map +1 -0
  195. package/dist/utils/signature-verifier.js +238 -0
  196. package/dist/utils/signature-verifier.js.map +1 -0
  197. package/dist/utils/ssrf-validator.d.ts +36 -0
  198. package/dist/utils/ssrf-validator.d.ts.map +1 -0
  199. package/dist/utils/ssrf-validator.js +195 -0
  200. package/dist/utils/ssrf-validator.js.map +1 -0
  201. package/examples/.env.example +17 -0
  202. package/examples/basic-usage.ts +211 -0
  203. package/examples/production-dashboard/.env.example +153 -0
  204. package/examples/production-dashboard/package.json +39 -0
  205. package/examples/production-dashboard/public/dashboard.html +642 -0
  206. package/examples/production-dashboard/server.ts +753 -0
  207. package/examples/webhook-integration.ts +239 -0
  208. package/examples/x-influencer-battle-redesign.html +1065 -0
  209. package/examples/x-influencer-battle-server.ts +217 -0
  210. package/examples/x-influencer-battle.html +787 -0
  211. package/package.json +65 -0
  212. package/src/constants.ts +43 -0
  213. package/src/core/websocket-client.test.ts +512 -0
  214. package/src/core/websocket-client.ts +1056 -0
  215. package/src/formatters/response-formatter.test.ts +571 -0
  216. package/src/formatters/response-formatter.ts +677 -0
  217. package/src/handlers/message-handler-registry.ts +239 -0
  218. package/src/handlers/message-handlers/agent-selected-handler.ts +40 -0
  219. package/src/handlers/message-handlers/agents-list-handler.ts +26 -0
  220. package/src/handlers/message-handlers/auth-error-handler.ts +31 -0
  221. package/src/handlers/message-handlers/auth-message-handler.ts +66 -0
  222. package/src/handlers/message-handlers/auth-required-handler.ts +23 -0
  223. package/src/handlers/message-handlers/auth-success-handler.ts +57 -0
  224. package/src/handlers/message-handlers/base-handler.ts +101 -0
  225. package/src/handlers/message-handlers/challenge-handler.ts +57 -0
  226. package/src/handlers/message-handlers/error-message-handler.ts +27 -0
  227. package/src/handlers/message-handlers/index.ts +77 -0
  228. package/src/handlers/message-handlers/list-rooms-response-handler.ts +28 -0
  229. package/src/handlers/message-handlers/ping-pong-handler.ts +30 -0
  230. package/src/handlers/message-handlers/regular-message-handler.ts +65 -0
  231. package/src/handlers/message-handlers/subscribe-response-handler.ts +47 -0
  232. package/src/handlers/message-handlers/task-response-handler.ts +45 -0
  233. package/src/handlers/message-handlers/types.ts +77 -0
  234. package/src/handlers/message-handlers/unsubscribe-response-handler.ts +47 -0
  235. package/src/handlers/webhook-handler.test.ts +789 -0
  236. package/src/handlers/webhook-handler.ts +576 -0
  237. package/src/index.ts +269 -0
  238. package/src/managers/agent-registry.test.ts +466 -0
  239. package/src/managers/agent-registry.ts +347 -0
  240. package/src/managers/connection-manager.ts +195 -0
  241. package/src/managers/index.ts +9 -0
  242. package/src/managers/message-router.ts +349 -0
  243. package/src/managers/room-manager.ts +248 -0
  244. package/src/teneo-sdk.ts +1022 -0
  245. package/src/types/config.test.ts +325 -0
  246. package/src/types/config.ts +799 -0
  247. package/src/types/error-codes.ts +44 -0
  248. package/src/types/events.test.ts +302 -0
  249. package/src/types/events.ts +382 -0
  250. package/src/types/health.ts +46 -0
  251. package/src/types/index.ts +199 -0
  252. package/src/types/messages.test.ts +660 -0
  253. package/src/types/messages.ts +570 -0
  254. package/src/types/validation.ts +123 -0
  255. package/src/utils/bounded-queue.test.ts +356 -0
  256. package/src/utils/bounded-queue.ts +205 -0
  257. package/src/utils/circuit-breaker.test.ts +394 -0
  258. package/src/utils/circuit-breaker.ts +262 -0
  259. package/src/utils/deduplication-cache.test.ts +380 -0
  260. package/src/utils/deduplication-cache.ts +198 -0
  261. package/src/utils/event-waiter.test.ts +381 -0
  262. package/src/utils/event-waiter.ts +172 -0
  263. package/src/utils/index.ts +74 -0
  264. package/src/utils/logger.ts +87 -0
  265. package/src/utils/rate-limiter.test.ts +341 -0
  266. package/src/utils/rate-limiter.ts +211 -0
  267. package/src/utils/retry-policy.test.ts +558 -0
  268. package/src/utils/retry-policy.ts +272 -0
  269. package/src/utils/secure-private-key.test.ts +356 -0
  270. package/src/utils/secure-private-key.ts +205 -0
  271. package/src/utils/signature-verifier.test.ts +464 -0
  272. package/src/utils/signature-verifier.ts +298 -0
  273. package/src/utils/ssrf-validator.test.ts +372 -0
  274. package/src/utils/ssrf-validator.ts +224 -0
  275. package/tests/integration/real-server.test.ts +740 -0
  276. package/tests/integration/websocket.test.ts +381 -0
  277. package/tests/integration-setup.ts +16 -0
  278. package/tests/setup.ts +34 -0
  279. package/tsconfig.json +32 -0
  280. package/vitest.config.ts +42 -0
  281. package/vitest.integration.config.ts +23 -0
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Secure private key storage with in-memory encryption
3
+ * Addresses SEC-3: Private Key Exposure Risk
4
+ *
5
+ * This class encrypts private keys in memory using AES-256-GCM to prevent
6
+ * exposure through memory dumps, heap snapshots, or accidental logging.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * const secureKey = new SecurePrivateKey(privateKey);
11
+ *
12
+ * // Use the key temporarily for signing
13
+ * const signature = secureKey.use((key) => {
14
+ * return signMessage(key);
15
+ * });
16
+ *
17
+ * // Clean up when done
18
+ * secureKey.destroy();
19
+ * ```
20
+ */
21
+
22
+ import crypto from 'crypto';
23
+
24
+ /**
25
+ * Securely stores and manages an encrypted private key in memory.
26
+ *
27
+ * The key is encrypted immediately upon construction and only decrypted
28
+ * temporarily when needed for operations like signing. Decrypted keys
29
+ * are zeroed out immediately after use.
30
+ */
31
+ export class SecurePrivateKey {
32
+ private encrypted: Buffer;
33
+ private encryptionKey: Buffer;
34
+ private destroyed = false;
35
+
36
+ /**
37
+ * Creates a new secure private key storage.
38
+ * The provided private key is encrypted immediately and the original
39
+ * string becomes eligible for garbage collection.
40
+ *
41
+ * @param privateKey - The private key to encrypt and store securely
42
+ * @throws {Error} If private key is empty or invalid
43
+ */
44
+ constructor(privateKey: string) {
45
+ if (!privateKey || typeof privateKey !== 'string') {
46
+ throw new Error('Private key must be a non-empty string');
47
+ }
48
+
49
+ // Generate a random encryption key for AES-256
50
+ this.encryptionKey = crypto.randomBytes(32);
51
+
52
+ // Encrypt the private key immediately
53
+ this.encrypted = this.encrypt(privateKey);
54
+
55
+ // Original privateKey string will be garbage collected
56
+ }
57
+
58
+ /**
59
+ * Temporarily decrypts the private key and passes it to the provided function.
60
+ * The decrypted key is automatically zeroed out after the function completes,
61
+ * whether it succeeds or throws an error.
62
+ *
63
+ * This is the only way to access the decrypted private key, ensuring minimal
64
+ * exposure time in plaintext.
65
+ *
66
+ * @template T - The return type of the function
67
+ * @param fn - Function that uses the decrypted private key
68
+ * @returns The result of the function
69
+ * @throws {Error} If the key has been destroyed
70
+ * @throws Any error thrown by the provided function
71
+ *
72
+ * @example
73
+ * ```typescript
74
+ * const account = secureKey.use((key) => privateKeyToAccount(key));
75
+ * const signature = secureKey.use((key) => account.signMessage(key));
76
+ * ```
77
+ */
78
+ public use<T>(fn: (key: string) => T): T {
79
+ this.checkNotDestroyed();
80
+
81
+ const decrypted = this.decrypt();
82
+ try {
83
+ return fn(decrypted);
84
+ } finally {
85
+ // Zero out the decrypted string in memory
86
+ // Note: This is best-effort as JavaScript strings are immutable
87
+ // but we overwrite the backing buffer
88
+ this.zeroOutString(decrypted);
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Destroys this secure key by zeroing out all sensitive buffers.
94
+ * After calling destroy(), this instance can no longer be used.
95
+ *
96
+ * This should be called when the SDK is disconnected or the key
97
+ * is no longer needed to prevent memory exposure.
98
+ *
99
+ * @example
100
+ * ```typescript
101
+ * secureKey.destroy();
102
+ * // secureKey.use(...) will now throw an error
103
+ * ```
104
+ */
105
+ public destroy(): void {
106
+ if (this.destroyed) {
107
+ return;
108
+ }
109
+
110
+ // Zero out all sensitive buffers
111
+ this.encryptionKey.fill(0);
112
+ this.encrypted.fill(0);
113
+
114
+ this.destroyed = true;
115
+ }
116
+
117
+ /**
118
+ * Checks if this secure key has been destroyed.
119
+ *
120
+ * @returns True if destroyed, false otherwise
121
+ */
122
+ public isDestroyed(): boolean {
123
+ return this.destroyed;
124
+ }
125
+
126
+ /**
127
+ * Encrypts the private key using AES-256-GCM.
128
+ *
129
+ * The encrypted buffer format is: [IV (16 bytes) | Auth Tag (16 bytes) | Ciphertext]
130
+ *
131
+ * @param data - The private key to encrypt
132
+ * @returns Buffer containing IV + auth tag + encrypted data
133
+ */
134
+ private encrypt(data: string): Buffer {
135
+ // Generate random initialization vector
136
+ const iv = crypto.randomBytes(16);
137
+
138
+ // Create cipher
139
+ const cipher = crypto.createCipheriv('aes-256-gcm', this.encryptionKey, iv);
140
+
141
+ // Encrypt the data
142
+ const encrypted = Buffer.concat([
143
+ cipher.update(data, 'utf8'),
144
+ cipher.final()
145
+ ]);
146
+
147
+ // Get authentication tag for integrity verification
148
+ const authTag = cipher.getAuthTag();
149
+
150
+ // Combine IV + authTag + encrypted data
151
+ return Buffer.concat([iv, authTag, encrypted]);
152
+ }
153
+
154
+ /**
155
+ * Decrypts the stored private key.
156
+ *
157
+ * @returns The decrypted private key as a string
158
+ * @throws {Error} If decryption fails (tampered data or wrong key)
159
+ */
160
+ private decrypt(): string {
161
+ // Extract components from encrypted buffer
162
+ const iv = this.encrypted.subarray(0, 16);
163
+ const authTag = this.encrypted.subarray(16, 32);
164
+ const ciphertext = this.encrypted.subarray(32);
165
+
166
+ // Create decipher
167
+ const decipher = crypto.createDecipheriv('aes-256-gcm', this.encryptionKey, iv);
168
+ decipher.setAuthTag(authTag);
169
+
170
+ // Decrypt the data
171
+ const decrypted = Buffer.concat([
172
+ decipher.update(ciphertext),
173
+ decipher.final()
174
+ ]);
175
+
176
+ return decrypted.toString('utf8');
177
+ }
178
+
179
+ /**
180
+ * Best-effort attempt to zero out a string in memory.
181
+ * JavaScript strings are immutable, but we can try to overwrite
182
+ * the backing buffer if it exists.
183
+ *
184
+ * @param str - The string to zero out
185
+ */
186
+ private zeroOutString(str: string): void {
187
+ // Convert to buffer and zero it out
188
+ const buffer = Buffer.from(str, 'utf8');
189
+ buffer.fill(0);
190
+
191
+ // Note: The original string object may still exist in memory
192
+ // until garbage collected, but this reduces the attack surface
193
+ }
194
+
195
+ /**
196
+ * Checks if this instance has been destroyed and throws if so.
197
+ *
198
+ * @throws {Error} If the key has been destroyed
199
+ */
200
+ private checkNotDestroyed(): void {
201
+ if (this.destroyed) {
202
+ throw new Error('SecurePrivateKey has been destroyed and can no longer be used');
203
+ }
204
+ }
205
+ }
@@ -0,0 +1,464 @@
1
+ /**
2
+ * Tests for Signature Verifier
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach } from "vitest";
6
+ import { privateKeyToAccount } from "viem/accounts";
7
+ import { SignatureVerifier } from "./signature-verifier";
8
+ import { BaseMessage, createUserMessage } from "../types";
9
+
10
+ describe("SignatureVerifier", () => {
11
+ // Test accounts
12
+ const testAccount1 = privateKeyToAccount('0x1234567890123456789012345678901234567890123456789012345678901234');
13
+ const testAccount2 = privateKeyToAccount('0xabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd');
14
+
15
+ describe("constructor", () => {
16
+ it("should create verifier with default options", () => {
17
+ const verifier = new SignatureVerifier();
18
+ const options = verifier.getOptions();
19
+
20
+ expect(options.trustedAddresses).toEqual([]);
21
+ expect(options.requireSignaturesFor).toEqual([]);
22
+ expect(options.strictMode).toBe(false);
23
+ });
24
+
25
+ it("should create verifier with custom options", () => {
26
+ const verifier = new SignatureVerifier({
27
+ trustedAddresses: [testAccount1.address],
28
+ requireSignaturesFor: ['task_response', 'agent_selected'],
29
+ strictMode: true
30
+ });
31
+
32
+ const options = verifier.getOptions();
33
+ expect(options.trustedAddresses).toEqual([testAccount1.address]);
34
+ expect(options.requireSignaturesFor).toEqual(['task_response', 'agent_selected']);
35
+ expect(options.strictMode).toBe(true);
36
+ });
37
+ });
38
+
39
+ describe("getSignableContent", () => {
40
+ it("should exclude signature and publicKey fields", () => {
41
+ const verifier = new SignatureVerifier();
42
+ const message: BaseMessage = {
43
+ type: 'message',
44
+ content: 'Hello',
45
+ from: 'agent1',
46
+ signature: '0xsignature',
47
+ publicKey: '0xpublickey',
48
+ timestamp: '2024-01-01T00:00:00Z'
49
+ };
50
+
51
+ const signable = verifier.getSignableContent(message);
52
+
53
+ expect(signable).not.toHaveProperty('signature');
54
+ expect(signable).not.toHaveProperty('publicKey');
55
+ expect(signable).not.toHaveProperty('id');
56
+ expect(signable).toHaveProperty('type');
57
+ expect(signable).toHaveProperty('content');
58
+ expect(signable).toHaveProperty('from');
59
+ });
60
+
61
+ it("should exclude undefined fields", () => {
62
+ const verifier = new SignatureVerifier();
63
+ const message: BaseMessage = {
64
+ type: 'message',
65
+ content: 'Hello',
66
+ from: undefined,
67
+ room: 'general'
68
+ };
69
+
70
+ const signable = verifier.getSignableContent(message);
71
+
72
+ expect(signable).not.toHaveProperty('from');
73
+ expect(signable).toHaveProperty('type');
74
+ expect(signable).toHaveProperty('content');
75
+ expect(signable).toHaveProperty('room');
76
+ });
77
+ });
78
+
79
+ describe("createMessageHash", () => {
80
+ it("should create consistent hashes for same content", () => {
81
+ const verifier = new SignatureVerifier();
82
+ const content = { type: 'message', content: 'Hello' };
83
+
84
+ const hash1 = verifier.createMessageHash(content);
85
+ const hash2 = verifier.createMessageHash(content);
86
+
87
+ expect(hash1).toBe(hash2);
88
+ });
89
+
90
+ it("should create different hashes for different content", () => {
91
+ const verifier = new SignatureVerifier();
92
+ const content1 = { type: 'message', content: 'Hello' };
93
+ const content2 = { type: 'message', content: 'World' };
94
+
95
+ const hash1 = verifier.createMessageHash(content1);
96
+ const hash2 = verifier.createMessageHash(content2);
97
+
98
+ expect(hash1).not.toBe(hash2);
99
+ });
100
+
101
+ it("should create same hash regardless of key order", () => {
102
+ const verifier = new SignatureVerifier();
103
+ const content1 = { type: 'message', content: 'Hello', from: 'agent' };
104
+ const content2 = { from: 'agent', content: 'Hello', type: 'message' };
105
+
106
+ const hash1 = verifier.createMessageHash(content1);
107
+ const hash2 = verifier.createMessageHash(content2);
108
+
109
+ expect(hash1).toBe(hash2);
110
+ });
111
+ });
112
+
113
+ describe("isSignatureRequired", () => {
114
+ it("should return false when message type not in required list", () => {
115
+ const verifier = new SignatureVerifier({
116
+ requireSignaturesFor: ['task_response']
117
+ });
118
+
119
+ expect(verifier.isSignatureRequired('message')).toBe(false);
120
+ expect(verifier.isSignatureRequired('ping')).toBe(false);
121
+ });
122
+
123
+ it("should return true when message type is in required list", () => {
124
+ const verifier = new SignatureVerifier({
125
+ requireSignaturesFor: ['task_response', 'agent_selected']
126
+ });
127
+
128
+ expect(verifier.isSignatureRequired('task_response')).toBe(true);
129
+ expect(verifier.isSignatureRequired('agent_selected')).toBe(true);
130
+ });
131
+
132
+ it("should return false when no required types configured", () => {
133
+ const verifier = new SignatureVerifier();
134
+
135
+ expect(verifier.isSignatureRequired('task_response')).toBe(false);
136
+ expect(verifier.isSignatureRequired('message')).toBe(false);
137
+ });
138
+ });
139
+
140
+ describe("isTrustedAddress", () => {
141
+ it("should return true when no whitelist configured", () => {
142
+ const verifier = new SignatureVerifier();
143
+
144
+ expect(verifier.isTrustedAddress(testAccount1.address)).toBe(true);
145
+ expect(verifier.isTrustedAddress(testAccount2.address)).toBe(true);
146
+ });
147
+
148
+ it("should return true for addresses in whitelist", () => {
149
+ const verifier = new SignatureVerifier({
150
+ trustedAddresses: [testAccount1.address]
151
+ });
152
+
153
+ expect(verifier.isTrustedAddress(testAccount1.address)).toBe(true);
154
+ });
155
+
156
+ it("should return false for addresses not in whitelist", () => {
157
+ const verifier = new SignatureVerifier({
158
+ trustedAddresses: [testAccount1.address]
159
+ });
160
+
161
+ expect(verifier.isTrustedAddress(testAccount2.address)).toBe(false);
162
+ });
163
+
164
+ it("should be case-insensitive", () => {
165
+ const verifier = new SignatureVerifier({
166
+ trustedAddresses: [testAccount1.address.toLowerCase()]
167
+ });
168
+
169
+ expect(verifier.isTrustedAddress(testAccount1.address.toUpperCase())).toBe(true);
170
+ });
171
+ });
172
+
173
+ describe("verify - missing signature", () => {
174
+ it("should pass when signature missing and not required", async () => {
175
+ const verifier = new SignatureVerifier({
176
+ strictMode: false
177
+ });
178
+
179
+ const message: BaseMessage = {
180
+ type: 'ping'
181
+ };
182
+
183
+ const result = await verifier.verify(message);
184
+
185
+ expect(result.valid).toBe(true);
186
+ expect(result.signatureMissing).toBe(true);
187
+ });
188
+
189
+ it("should fail when signature missing but required for message type", async () => {
190
+ const verifier = new SignatureVerifier({
191
+ requireSignaturesFor: ['task_response']
192
+ });
193
+
194
+ const message: BaseMessage = {
195
+ type: 'task_response',
196
+ content: 'Result'
197
+ };
198
+
199
+ const result = await verifier.verify(message);
200
+
201
+ expect(result.valid).toBe(false);
202
+ expect(result.signatureMissing).toBe(true);
203
+ expect(result.reason).toContain('required');
204
+ });
205
+
206
+ it("should fail when signature missing and strictMode enabled", async () => {
207
+ const verifier = new SignatureVerifier({
208
+ strictMode: true
209
+ });
210
+
211
+ const message: BaseMessage = {
212
+ type: 'message',
213
+ content: 'Hello'
214
+ };
215
+
216
+ const result = await verifier.verify(message);
217
+
218
+ expect(result.valid).toBe(false);
219
+ expect(result.signatureMissing).toBe(true);
220
+ });
221
+ });
222
+
223
+ describe("verify - with signature", () => {
224
+ it("should verify valid signature", async () => {
225
+ const verifier = new SignatureVerifier();
226
+
227
+ const message: BaseMessage = {
228
+ type: 'message',
229
+ content: 'Hello',
230
+ from: 'agent1',
231
+ timestamp: '2024-01-01T00:00:00Z',
232
+ publicKey: testAccount1.address
233
+ };
234
+
235
+ const signableContent = verifier.getSignableContent(message);
236
+ const messageHash = verifier.createMessageHash(signableContent);
237
+ const signature = await testAccount1.signMessage({ message: messageHash });
238
+
239
+ const messageWithSignature: BaseMessage = {
240
+ ...message,
241
+ signature
242
+ };
243
+
244
+ const result = await verifier.verify(messageWithSignature);
245
+
246
+ expect(result.valid).toBe(true);
247
+ expect(result.signatureMissing).toBe(false);
248
+ expect(result.recoveredAddress).toBe(testAccount1.address);
249
+ });
250
+
251
+ it("should reject invalid signature", async () => {
252
+ const verifier = new SignatureVerifier();
253
+
254
+ const message: BaseMessage = {
255
+ type: 'message',
256
+ content: 'Hello',
257
+ publicKey: testAccount1.address,
258
+ signature: '0xinvalidsignature1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012'
259
+ };
260
+
261
+ const result = await verifier.verify(message);
262
+
263
+ expect(result.valid).toBe(false);
264
+ expect(result.signatureMissing).toBe(false);
265
+ expect(result.reason).toContain('error');
266
+ });
267
+
268
+ it("should reject signature from wrong account", async () => {
269
+ const verifier = new SignatureVerifier();
270
+
271
+ const message: BaseMessage = {
272
+ type: 'message',
273
+ content: 'Hello',
274
+ publicKey: testAccount1.address
275
+ };
276
+
277
+ const signableContent = verifier.getSignableContent(message);
278
+ const messageHash = verifier.createMessageHash(signableContent);
279
+
280
+ // Sign with account2 but verify against account1
281
+ const signature = await testAccount2.signMessage({ message: messageHash });
282
+
283
+ const messageWithSignature: BaseMessage = {
284
+ ...message,
285
+ signature
286
+ };
287
+
288
+ const result = await verifier.verify(messageWithSignature);
289
+
290
+ expect(result.valid).toBe(false);
291
+ expect(result.reason).toContain('does not match');
292
+ });
293
+ });
294
+
295
+ describe("verify - whitelist", () => {
296
+ it("should accept message from trusted address", async () => {
297
+ const verifier = new SignatureVerifier({
298
+ trustedAddresses: [testAccount1.address]
299
+ });
300
+
301
+ const message: BaseMessage = {
302
+ type: 'message',
303
+ content: 'Hello',
304
+ publicKey: testAccount1.address
305
+ };
306
+
307
+ const signableContent = verifier.getSignableContent(message);
308
+ const messageHash = verifier.createMessageHash(signableContent);
309
+ const signature = await testAccount1.signMessage({ message: messageHash });
310
+
311
+ const messageWithSignature: BaseMessage = {
312
+ ...message,
313
+ signature
314
+ };
315
+
316
+ const result = await verifier.verify(messageWithSignature);
317
+
318
+ expect(result.valid).toBe(true);
319
+ expect(result.isTrusted).toBe(true);
320
+ });
321
+
322
+ it("should reject message from untrusted address", async () => {
323
+ const verifier = new SignatureVerifier({
324
+ trustedAddresses: [testAccount1.address]
325
+ });
326
+
327
+ const message: BaseMessage = {
328
+ type: 'message',
329
+ content: 'Hello',
330
+ publicKey: testAccount2.address
331
+ };
332
+
333
+ const signableContent = verifier.getSignableContent(message);
334
+ const messageHash = verifier.createMessageHash(signableContent);
335
+ const signature = await testAccount2.signMessage({ message: messageHash });
336
+
337
+ const messageWithSignature: BaseMessage = {
338
+ ...message,
339
+ signature
340
+ };
341
+
342
+ const result = await verifier.verify(messageWithSignature);
343
+
344
+ expect(result.valid).toBe(false);
345
+ expect(result.isTrusted).toBe(false);
346
+ expect(result.reason).toContain('not in trusted whitelist');
347
+ });
348
+ });
349
+
350
+ describe("verify - address sources", () => {
351
+ it("should use publicKey field for verification", async () => {
352
+ const verifier = new SignatureVerifier();
353
+
354
+ const message: BaseMessage = {
355
+ type: 'message',
356
+ content: 'Hello',
357
+ publicKey: testAccount1.address
358
+ };
359
+
360
+ const signableContent = verifier.getSignableContent(message);
361
+ const messageHash = verifier.createMessageHash(signableContent);
362
+ const signature = await testAccount1.signMessage({ message: messageHash });
363
+
364
+ const messageWithSignature: BaseMessage = {
365
+ ...message,
366
+ signature
367
+ };
368
+
369
+ const result = await verifier.verify(messageWithSignature);
370
+
371
+ expect(result.valid).toBe(true);
372
+ expect(result.recoveredAddress).toBe(testAccount1.address);
373
+ });
374
+
375
+ it("should fallback to from field if address-like", async () => {
376
+ const verifier = new SignatureVerifier();
377
+
378
+ const message: BaseMessage = {
379
+ type: 'message',
380
+ content: 'Hello',
381
+ from: testAccount1.address
382
+ };
383
+
384
+ const signableContent = verifier.getSignableContent(message);
385
+ const messageHash = verifier.createMessageHash(signableContent);
386
+ const signature = await testAccount1.signMessage({ message: messageHash });
387
+
388
+ const messageWithSignature: BaseMessage = {
389
+ ...message,
390
+ signature
391
+ };
392
+
393
+ const result = await verifier.verify(messageWithSignature);
394
+
395
+ expect(result.valid).toBe(true);
396
+ expect(result.recoveredAddress).toBe(testAccount1.address);
397
+ });
398
+
399
+ it("should fail if no valid address available", async () => {
400
+ const verifier = new SignatureVerifier();
401
+
402
+ const message: BaseMessage = {
403
+ type: 'message',
404
+ content: 'Hello',
405
+ from: 'agent-name-not-address',
406
+ signature: '0xsignature'
407
+ };
408
+
409
+ const result = await verifier.verify(message);
410
+
411
+ expect(result.valid).toBe(false);
412
+ expect(result.reason).toContain('No address available');
413
+ });
414
+ });
415
+
416
+ describe("updateOptions", () => {
417
+ it("should update trusted addresses", () => {
418
+ const verifier = new SignatureVerifier();
419
+
420
+ verifier.updateOptions({
421
+ trustedAddresses: [testAccount1.address]
422
+ });
423
+
424
+ const options = verifier.getOptions();
425
+ expect(options.trustedAddresses).toEqual([testAccount1.address]);
426
+ });
427
+
428
+ it("should update required message types", () => {
429
+ const verifier = new SignatureVerifier();
430
+
431
+ verifier.updateOptions({
432
+ requireSignaturesFor: ['task_response']
433
+ });
434
+
435
+ expect(verifier.isSignatureRequired('task_response')).toBe(true);
436
+ });
437
+
438
+ it("should update strict mode", () => {
439
+ const verifier = new SignatureVerifier({ strictMode: false });
440
+
441
+ verifier.updateOptions({
442
+ strictMode: true
443
+ });
444
+
445
+ const options = verifier.getOptions();
446
+ expect(options.strictMode).toBe(true);
447
+ });
448
+
449
+ it("should merge with existing options", () => {
450
+ const verifier = new SignatureVerifier({
451
+ trustedAddresses: [testAccount1.address],
452
+ strictMode: false
453
+ });
454
+
455
+ verifier.updateOptions({
456
+ strictMode: true
457
+ });
458
+
459
+ const options = verifier.getOptions();
460
+ expect(options.trustedAddresses).toEqual([testAccount1.address]);
461
+ expect(options.strictMode).toBe(true);
462
+ });
463
+ });
464
+ });