@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,272 @@
1
+ /**
2
+ * RetryPolicy - Configurable retry strategy for connection and webhook retries
3
+ * Supports exponential, linear, and constant backoff strategies
4
+ */
5
+
6
+ import { z } from "zod";
7
+
8
+ /**
9
+ * Retry strategy types
10
+ */
11
+ export type RetryStrategyType = "exponential" | "linear" | "constant";
12
+
13
+ /**
14
+ * Retry strategy configuration
15
+ */
16
+ export interface RetryStrategy {
17
+ /** Strategy type: exponential, linear, or constant */
18
+ type: RetryStrategyType;
19
+ /** Base delay in milliseconds (first retry delay) */
20
+ baseDelay: number;
21
+ /** Maximum delay cap in milliseconds */
22
+ maxDelay: number;
23
+ /** Maximum number of retry attempts (0 = no retries) */
24
+ maxAttempts: number;
25
+ /** Add random jitter to prevent thundering herd */
26
+ jitter: boolean;
27
+ /** Backoff multiplier for exponential strategy (default: 2) */
28
+ backoffMultiplier?: number;
29
+ }
30
+
31
+ /**
32
+ * Zod schema for retry strategy validation
33
+ */
34
+ export const RetryStrategySchema = z.object({
35
+ type: z.enum(["exponential", "linear", "constant"]),
36
+ baseDelay: z.number().min(0).max(300000), // 0-5 minutes
37
+ maxDelay: z.number().min(0).max(3600000), // 0-1 hour
38
+ maxAttempts: z.number().min(0).max(1000),
39
+ jitter: z.boolean(),
40
+ backoffMultiplier: z.number().min(1).max(10).optional()
41
+ });
42
+
43
+ /**
44
+ * RetryPolicy - Calculates retry delays and determines retry eligibility
45
+ *
46
+ * Supports three retry strategies:
47
+ * - **Exponential**: Delay increases exponentially (e.g., 1s, 2s, 4s, 8s, ...)
48
+ * - **Linear**: Delay increases linearly (e.g., 1s, 2s, 3s, 4s, ...)
49
+ * - **Constant**: Delay remains constant (e.g., 1s, 1s, 1s, 1s, ...)
50
+ *
51
+ * All strategies respect maxDelay cap and support optional jitter.
52
+ *
53
+ * @example
54
+ * ```typescript
55
+ * // Exponential backoff with default multiplier of 2
56
+ * const policy = RetryPolicy.exponential(1000, 60000, 10, true);
57
+ * console.log(policy.calculateDelay(1)); // ~1000ms
58
+ * console.log(policy.calculateDelay(2)); // ~2000ms
59
+ * console.log(policy.calculateDelay(3)); // ~4000ms
60
+ *
61
+ * // Linear backoff
62
+ * const policy = RetryPolicy.linear(2000, 30000, 5, false);
63
+ * console.log(policy.calculateDelay(1)); // 2000ms
64
+ * console.log(policy.calculateDelay(2)); // 4000ms
65
+ * console.log(policy.calculateDelay(3)); // 6000ms
66
+ *
67
+ * // Constant delay
68
+ * const policy = RetryPolicy.constant(5000, 3);
69
+ * console.log(policy.calculateDelay(1)); // 5000ms
70
+ * console.log(policy.calculateDelay(2)); // 5000ms
71
+ * console.log(policy.calculateDelay(3)); // 5000ms
72
+ * ```
73
+ */
74
+ export class RetryPolicy {
75
+ private readonly strategy: RetryStrategy;
76
+
77
+ /**
78
+ * Creates a new retry policy with the specified strategy
79
+ *
80
+ * @param strategy - Retry strategy configuration
81
+ * @throws {z.ZodError} If strategy is invalid
82
+ */
83
+ constructor(strategy: RetryStrategy) {
84
+ // Validate strategy with Zod
85
+ this.strategy = RetryStrategySchema.parse(strategy);
86
+
87
+ // Additional validation: maxDelay must be >= baseDelay
88
+ if (this.strategy.maxDelay < this.strategy.baseDelay) {
89
+ throw new Error("maxDelay must be greater than or equal to baseDelay");
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Calculates the retry delay for a given attempt number
95
+ *
96
+ * @param attempt - Attempt number (1-indexed: 1 = first retry)
97
+ * @returns Delay in milliseconds before next retry
98
+ *
99
+ * @example
100
+ * ```typescript
101
+ * const policy = RetryPolicy.exponential(1000, 60000, 10, true);
102
+ *
103
+ * // First retry after 1st failure
104
+ * policy.calculateDelay(1); // ~1000ms (+ jitter if enabled)
105
+ *
106
+ * // Second retry after 2nd failure
107
+ * policy.calculateDelay(2); // ~2000ms (+ jitter if enabled)
108
+ * ```
109
+ */
110
+ public calculateDelay(attempt: number): number {
111
+ // Validate attempt number
112
+ if (attempt < 1) {
113
+ throw new Error("Attempt number must be >= 1");
114
+ }
115
+
116
+ let delay: number;
117
+
118
+ switch (this.strategy.type) {
119
+ case "exponential": {
120
+ const multiplier = this.strategy.backoffMultiplier || 2;
121
+ delay = this.strategy.baseDelay * Math.pow(multiplier, attempt - 1);
122
+ break;
123
+ }
124
+ case "linear": {
125
+ delay = this.strategy.baseDelay * attempt;
126
+ break;
127
+ }
128
+ case "constant": {
129
+ delay = this.strategy.baseDelay;
130
+ break;
131
+ }
132
+ }
133
+
134
+ // Cap at maxDelay
135
+ delay = Math.min(delay, this.strategy.maxDelay);
136
+
137
+ // Add jitter if enabled
138
+ if (this.strategy.jitter) {
139
+ // Add random jitter between 0 and 1000ms
140
+ delay += Math.random() * 1000;
141
+ }
142
+
143
+ return Math.floor(delay);
144
+ }
145
+
146
+ /**
147
+ * Determines if a retry should be attempted based on attempt count
148
+ *
149
+ * @param attempt - Current attempt number (1-indexed)
150
+ * @returns True if retry should be attempted, false otherwise
151
+ *
152
+ * @example
153
+ * ```typescript
154
+ * const policy = RetryPolicy.exponential(1000, 60000, 3, false);
155
+ *
156
+ * policy.shouldRetry(1); // true
157
+ * policy.shouldRetry(2); // true
158
+ * policy.shouldRetry(3); // true
159
+ * policy.shouldRetry(4); // false - exceeded maxAttempts
160
+ * ```
161
+ */
162
+ public shouldRetry(attempt: number): boolean {
163
+ return attempt <= this.strategy.maxAttempts;
164
+ }
165
+
166
+ /**
167
+ * Gets the retry strategy configuration
168
+ *
169
+ * @returns Copy of the retry strategy
170
+ */
171
+ public getStrategy(): Readonly<RetryStrategy> {
172
+ return { ...this.strategy };
173
+ }
174
+
175
+ // Static factory methods for common patterns
176
+
177
+ /**
178
+ * Creates a retry policy with exponential backoff
179
+ *
180
+ * Delay formula: `min(baseDelay * multiplier^(attempt-1), maxDelay) + jitter`
181
+ *
182
+ * @param baseDelay - Initial delay in milliseconds
183
+ * @param maxDelay - Maximum delay cap in milliseconds
184
+ * @param maxAttempts - Maximum retry attempts
185
+ * @param jitter - Add random jitter (0-1000ms)
186
+ * @param backoffMultiplier - Exponential multiplier (default: 2)
187
+ * @returns RetryPolicy instance with exponential strategy
188
+ *
189
+ * @example
190
+ * ```typescript
191
+ * // Standard exponential backoff (2^n)
192
+ * const policy = RetryPolicy.exponential(1000, 60000, 10, true);
193
+ *
194
+ * // Faster exponential backoff (3^n)
195
+ * const aggressive = RetryPolicy.exponential(1000, 60000, 10, true, 3);
196
+ * ```
197
+ */
198
+ public static exponential(
199
+ baseDelay: number,
200
+ maxDelay: number,
201
+ maxAttempts: number,
202
+ jitter: boolean,
203
+ backoffMultiplier: number = 2
204
+ ): RetryPolicy {
205
+ return new RetryPolicy({
206
+ type: "exponential",
207
+ baseDelay,
208
+ maxDelay,
209
+ maxAttempts,
210
+ jitter,
211
+ backoffMultiplier
212
+ });
213
+ }
214
+
215
+ /**
216
+ * Creates a retry policy with linear backoff
217
+ *
218
+ * Delay formula: `min(baseDelay * attempt, maxDelay) + jitter`
219
+ *
220
+ * @param baseDelay - Delay increment per attempt in milliseconds
221
+ * @param maxDelay - Maximum delay cap in milliseconds
222
+ * @param maxAttempts - Maximum retry attempts
223
+ * @param jitter - Add random jitter (0-1000ms)
224
+ * @returns RetryPolicy instance with linear strategy
225
+ *
226
+ * @example
227
+ * ```typescript
228
+ * // Linear backoff: 2s, 4s, 6s, 8s, ...
229
+ * const policy = RetryPolicy.linear(2000, 30000, 10, false);
230
+ * ```
231
+ */
232
+ public static linear(
233
+ baseDelay: number,
234
+ maxDelay: number,
235
+ maxAttempts: number,
236
+ jitter: boolean
237
+ ): RetryPolicy {
238
+ return new RetryPolicy({
239
+ type: "linear",
240
+ baseDelay,
241
+ maxDelay,
242
+ maxAttempts,
243
+ jitter
244
+ });
245
+ }
246
+
247
+ /**
248
+ * Creates a retry policy with constant delay
249
+ *
250
+ * Delay formula: `baseDelay + jitter`
251
+ *
252
+ * @param delay - Constant delay in milliseconds
253
+ * @param maxAttempts - Maximum retry attempts
254
+ * @param jitter - Add random jitter (0-1000ms, default: false)
255
+ * @returns RetryPolicy instance with constant strategy
256
+ *
257
+ * @example
258
+ * ```typescript
259
+ * // Retry every 5 seconds, max 3 times
260
+ * const policy = RetryPolicy.constant(5000, 3);
261
+ * ```
262
+ */
263
+ public static constant(delay: number, maxAttempts: number, jitter: boolean = false): RetryPolicy {
264
+ return new RetryPolicy({
265
+ type: "constant",
266
+ baseDelay: delay,
267
+ maxDelay: delay,
268
+ maxAttempts,
269
+ jitter
270
+ });
271
+ }
272
+ }
@@ -0,0 +1,356 @@
1
+ /**
2
+ * Tests for SecurePrivateKey class
3
+ * Verifies secure encryption, decryption, memory cleanup, and integration with viem
4
+ */
5
+
6
+ import { SecurePrivateKey } from './secure-private-key';
7
+ import { privateKeyToAccount } from 'viem/accounts';
8
+
9
+ describe('SecurePrivateKey', () => {
10
+ // Test private key (do NOT use in production)
11
+ const testPrivateKey = '0x1234567890123456789012345678901234567890123456789012345678901234';
12
+
13
+ describe('constructor', () => {
14
+ it('should create instance with valid private key', () => {
15
+ const secureKey = new SecurePrivateKey(testPrivateKey);
16
+ expect(secureKey).toBeInstanceOf(SecurePrivateKey);
17
+ expect(secureKey.isDestroyed()).toBe(false);
18
+ secureKey.destroy();
19
+ });
20
+
21
+ it('should throw error with empty private key', () => {
22
+ expect(() => new SecurePrivateKey('')).toThrow('Private key must be a non-empty string');
23
+ });
24
+
25
+ it('should throw error with null private key', () => {
26
+ expect(() => new SecurePrivateKey(null as any)).toThrow('Private key must be a non-empty string');
27
+ });
28
+
29
+ it('should throw error with undefined private key', () => {
30
+ expect(() => new SecurePrivateKey(undefined as any)).toThrow(
31
+ 'Private key must be a non-empty string'
32
+ );
33
+ });
34
+
35
+ it('should throw error with non-string private key', () => {
36
+ expect(() => new SecurePrivateKey(123 as any)).toThrow('Private key must be a non-empty string');
37
+ });
38
+ });
39
+
40
+ describe('use()', () => {
41
+ it('should decrypt and pass key to callback', () => {
42
+ const secureKey = new SecurePrivateKey(testPrivateKey);
43
+
44
+ const result = secureKey.use((key) => {
45
+ expect(key).toBe(testPrivateKey);
46
+ return 'success';
47
+ });
48
+
49
+ expect(result).toBe('success');
50
+ secureKey.destroy();
51
+ });
52
+
53
+ it('should return callback result', () => {
54
+ const secureKey = new SecurePrivateKey(testPrivateKey);
55
+
56
+ const result = secureKey.use((key) => {
57
+ return { key: key.substring(0, 10), length: key.length };
58
+ });
59
+
60
+ expect(result).toEqual({ key: testPrivateKey.substring(0, 10), length: testPrivateKey.length });
61
+ secureKey.destroy();
62
+ });
63
+
64
+ it('should work with async callbacks', async () => {
65
+ const secureKey = new SecurePrivateKey(testPrivateKey);
66
+
67
+ const result = await secureKey.use(async (key) => {
68
+ await new Promise((resolve) => setTimeout(resolve, 10));
69
+ return key.length;
70
+ });
71
+
72
+ expect(result).toBe(testPrivateKey.length);
73
+ secureKey.destroy();
74
+ });
75
+
76
+ it('should allow multiple uses', () => {
77
+ const secureKey = new SecurePrivateKey(testPrivateKey);
78
+
79
+ const result1 = secureKey.use((key) => key.length);
80
+ const result2 = secureKey.use((key) => key.substring(0, 5));
81
+ const result3 = secureKey.use((key) => key);
82
+
83
+ expect(result1).toBe(testPrivateKey.length);
84
+ expect(result2).toBe(testPrivateKey.substring(0, 5));
85
+ expect(result3).toBe(testPrivateKey);
86
+
87
+ secureKey.destroy();
88
+ });
89
+
90
+ it('should throw if key has been destroyed', () => {
91
+ const secureKey = new SecurePrivateKey(testPrivateKey);
92
+ secureKey.destroy();
93
+
94
+ expect(() => {
95
+ secureKey.use((key) => key);
96
+ }).toThrow('SecurePrivateKey has been destroyed and can no longer be used');
97
+ });
98
+
99
+ it('should propagate errors from callback', () => {
100
+ const secureKey = new SecurePrivateKey(testPrivateKey);
101
+
102
+ expect(() => {
103
+ secureKey.use((key) => {
104
+ throw new Error('Test error');
105
+ });
106
+ }).toThrow('Test error');
107
+
108
+ secureKey.destroy();
109
+ });
110
+
111
+ it('should clean up even if callback throws', () => {
112
+ const secureKey = new SecurePrivateKey(testPrivateKey);
113
+
114
+ try {
115
+ secureKey.use((key) => {
116
+ throw new Error('Test error');
117
+ });
118
+ } catch (error) {
119
+ // Expected
120
+ }
121
+
122
+ // Key should still be usable after error
123
+ const result = secureKey.use((key) => key.length);
124
+ expect(result).toBe(testPrivateKey.length);
125
+
126
+ secureKey.destroy();
127
+ });
128
+ });
129
+
130
+ describe('destroy()', () => {
131
+ it('should mark instance as destroyed', () => {
132
+ const secureKey = new SecurePrivateKey(testPrivateKey);
133
+ expect(secureKey.isDestroyed()).toBe(false);
134
+
135
+ secureKey.destroy();
136
+ expect(secureKey.isDestroyed()).toBe(true);
137
+ });
138
+
139
+ it('should be idempotent (safe to call multiple times)', () => {
140
+ const secureKey = new SecurePrivateKey(testPrivateKey);
141
+
142
+ secureKey.destroy();
143
+ secureKey.destroy();
144
+ secureKey.destroy();
145
+
146
+ expect(secureKey.isDestroyed()).toBe(true);
147
+ });
148
+
149
+ it('should prevent further use after destruction', () => {
150
+ const secureKey = new SecurePrivateKey(testPrivateKey);
151
+ secureKey.destroy();
152
+
153
+ expect(() => {
154
+ secureKey.use((key) => key);
155
+ }).toThrow('SecurePrivateKey has been destroyed');
156
+ });
157
+ });
158
+
159
+ describe('isDestroyed()', () => {
160
+ it('should return false for new instance', () => {
161
+ const secureKey = new SecurePrivateKey(testPrivateKey);
162
+ expect(secureKey.isDestroyed()).toBe(false);
163
+ secureKey.destroy();
164
+ });
165
+
166
+ it('should return true after destruction', () => {
167
+ const secureKey = new SecurePrivateKey(testPrivateKey);
168
+ secureKey.destroy();
169
+ expect(secureKey.isDestroyed()).toBe(true);
170
+ });
171
+ });
172
+
173
+ describe('encryption/decryption', () => {
174
+ it('should correctly encrypt and decrypt private key', () => {
175
+ const secureKey = new SecurePrivateKey(testPrivateKey);
176
+
177
+ const decrypted = secureKey.use((key) => key);
178
+ expect(decrypted).toBe(testPrivateKey);
179
+
180
+ secureKey.destroy();
181
+ });
182
+
183
+ it('should handle keys with special characters', () => {
184
+ const specialKey = '0xABCDEF123456!@#$%^&*()_+-=[]{}|;:,.<>?';
185
+ const secureKey = new SecurePrivateKey(specialKey);
186
+
187
+ const decrypted = secureKey.use((key) => key);
188
+ expect(decrypted).toBe(specialKey);
189
+
190
+ secureKey.destroy();
191
+ });
192
+
193
+ it('should handle very long keys', () => {
194
+ const longKey = '0x' + 'a'.repeat(1000);
195
+ const secureKey = new SecurePrivateKey(longKey);
196
+
197
+ const decrypted = secureKey.use((key) => key);
198
+ expect(decrypted).toBe(longKey);
199
+
200
+ secureKey.destroy();
201
+ });
202
+
203
+ it('should produce different encrypted data for same key (random IV)', () => {
204
+ const secureKey1 = new SecurePrivateKey(testPrivateKey);
205
+ const secureKey2 = new SecurePrivateKey(testPrivateKey);
206
+
207
+ // Access private encrypted buffers via any to verify they're different
208
+ const encrypted1 = (secureKey1 as any).encrypted;
209
+ const encrypted2 = (secureKey2 as any).encrypted;
210
+
211
+ // Encrypted data should be different due to random IV
212
+ expect(Buffer.compare(encrypted1, encrypted2)).not.toBe(0);
213
+
214
+ // But decrypted should be the same
215
+ const decrypted1 = secureKey1.use((key) => key);
216
+ const decrypted2 = secureKey2.use((key) => key);
217
+
218
+ expect(decrypted1).toBe(testPrivateKey);
219
+ expect(decrypted2).toBe(testPrivateKey);
220
+
221
+ secureKey1.destroy();
222
+ secureKey2.destroy();
223
+ });
224
+ });
225
+
226
+ describe('integration with viem', () => {
227
+ it('should work with privateKeyToAccount', () => {
228
+ const secureKey = new SecurePrivateKey(testPrivateKey);
229
+
230
+ const account = secureKey.use((key) => {
231
+ return privateKeyToAccount(key as `0x${string}`);
232
+ });
233
+
234
+ expect(account).toBeDefined();
235
+ expect(account.address).toBeDefined();
236
+ expect(account.address).toMatch(/^0x[a-fA-F0-9]{40}$/);
237
+
238
+ secureKey.destroy();
239
+ });
240
+
241
+ it('should allow signing messages with account created from secure key', async () => {
242
+ const secureKey = new SecurePrivateKey(testPrivateKey);
243
+
244
+ const account = secureKey.use((key) => {
245
+ return privateKeyToAccount(key as `0x${string}`);
246
+ });
247
+
248
+ const message = 'Hello, Teneo!';
249
+ const signature = await account.signMessage({ message });
250
+
251
+ expect(signature).toBeDefined();
252
+ expect(signature).toMatch(/^0x[a-fA-F0-9]+$/);
253
+ expect(signature.length).toBeGreaterThan(100);
254
+
255
+ secureKey.destroy();
256
+ });
257
+
258
+ it('should create consistent account address across multiple uses', () => {
259
+ const secureKey = new SecurePrivateKey(testPrivateKey);
260
+
261
+ const address1 = secureKey.use((key) => {
262
+ return privateKeyToAccount(key as `0x${string}`).address;
263
+ });
264
+
265
+ const address2 = secureKey.use((key) => {
266
+ return privateKeyToAccount(key as `0x${string}`).address;
267
+ });
268
+
269
+ expect(address1).toBe(address2);
270
+
271
+ secureKey.destroy();
272
+ });
273
+ });
274
+
275
+ describe('security properties', () => {
276
+ it('should not expose private key in toString()', () => {
277
+ const secureKey = new SecurePrivateKey(testPrivateKey);
278
+
279
+ const stringified = String(secureKey);
280
+ expect(stringified).not.toContain(testPrivateKey);
281
+ expect(stringified).not.toContain(testPrivateKey.substring(5, 20));
282
+
283
+ secureKey.destroy();
284
+ });
285
+
286
+ it('should not expose private key in JSON.stringify()', () => {
287
+ const secureKey = new SecurePrivateKey(testPrivateKey);
288
+
289
+ const jsonString = JSON.stringify(secureKey);
290
+ expect(jsonString).not.toContain(testPrivateKey);
291
+ expect(jsonString).not.toContain(testPrivateKey.substring(5, 20));
292
+
293
+ secureKey.destroy();
294
+ });
295
+
296
+ it('should not expose private key when inspecting object', () => {
297
+ const secureKey = new SecurePrivateKey(testPrivateKey);
298
+
299
+ // Try to access private properties (they're still accessible via 'any' but not exposed)
300
+ const keys = Object.keys(secureKey);
301
+ const values = Object.values(secureKey);
302
+
303
+ // Should not contain the plaintext key
304
+ const allValues = JSON.stringify(values);
305
+ expect(allValues).not.toContain(testPrivateKey);
306
+
307
+ secureKey.destroy();
308
+ });
309
+
310
+ it('should store key encrypted in memory', () => {
311
+ const secureKey = new SecurePrivateKey(testPrivateKey);
312
+
313
+ // Access encrypted buffer via any
314
+ const encrypted = (secureKey as any).encrypted as Buffer;
315
+
316
+ // Encrypted data should not contain the plaintext key
317
+ const encryptedString = encrypted.toString('utf8');
318
+ expect(encryptedString).not.toContain(testPrivateKey);
319
+ expect(encryptedString).not.toContain('1234567890');
320
+
321
+ secureKey.destroy();
322
+ });
323
+ });
324
+
325
+ describe('memory cleanup', () => {
326
+ it('should zero out encryption key on destroy', () => {
327
+ const secureKey = new SecurePrivateKey(testPrivateKey);
328
+
329
+ // Get reference to encryption key before destroy
330
+ const encryptionKey = (secureKey as any).encryptionKey as Buffer;
331
+ const originalKeyData = Buffer.from(encryptionKey);
332
+
333
+ expect(originalKeyData.some((byte) => byte !== 0)).toBe(true);
334
+
335
+ secureKey.destroy();
336
+
337
+ // After destroy, encryption key should be zeroed
338
+ expect(encryptionKey.every((byte) => byte === 0)).toBe(true);
339
+ });
340
+
341
+ it('should zero out encrypted buffer on destroy', () => {
342
+ const secureKey = new SecurePrivateKey(testPrivateKey);
343
+
344
+ // Get reference to encrypted buffer before destroy
345
+ const encrypted = (secureKey as any).encrypted as Buffer;
346
+ const originalEncryptedData = Buffer.from(encrypted);
347
+
348
+ expect(originalEncryptedData.some((byte) => byte !== 0)).toBe(true);
349
+
350
+ secureKey.destroy();
351
+
352
+ // After destroy, encrypted buffer should be zeroed
353
+ expect(encrypted.every((byte) => byte === 0)).toBe(true);
354
+ });
355
+ });
356
+ });