@agirails/sdk 2.0.4 → 2.2.1

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 (201) hide show
  1. package/README.md +536 -87
  2. package/dist/ACTPClient.d.ts +200 -0
  3. package/dist/ACTPClient.d.ts.map +1 -1
  4. package/dist/ACTPClient.js +266 -2
  5. package/dist/ACTPClient.js.map +1 -1
  6. package/dist/abi/ACTPKernel.json +16 -0
  7. package/dist/adapters/AdapterRegistry.d.ts +140 -0
  8. package/dist/adapters/AdapterRegistry.d.ts.map +1 -0
  9. package/dist/adapters/AdapterRegistry.js +166 -0
  10. package/dist/adapters/AdapterRegistry.js.map +1 -0
  11. package/dist/adapters/AdapterRouter.d.ts +165 -0
  12. package/dist/adapters/AdapterRouter.d.ts.map +1 -0
  13. package/dist/adapters/AdapterRouter.js +350 -0
  14. package/dist/adapters/AdapterRouter.js.map +1 -0
  15. package/dist/adapters/BaseAdapter.d.ts +17 -0
  16. package/dist/adapters/BaseAdapter.d.ts.map +1 -1
  17. package/dist/adapters/BaseAdapter.js +21 -0
  18. package/dist/adapters/BaseAdapter.js.map +1 -1
  19. package/dist/adapters/BasicAdapter.d.ts +72 -3
  20. package/dist/adapters/BasicAdapter.d.ts.map +1 -1
  21. package/dist/adapters/BasicAdapter.js +178 -2
  22. package/dist/adapters/BasicAdapter.js.map +1 -1
  23. package/dist/adapters/IAdapter.d.ts +230 -0
  24. package/dist/adapters/IAdapter.d.ts.map +1 -0
  25. package/dist/adapters/IAdapter.js +44 -0
  26. package/dist/adapters/IAdapter.js.map +1 -0
  27. package/dist/adapters/StandardAdapter.d.ts +80 -6
  28. package/dist/adapters/StandardAdapter.d.ts.map +1 -1
  29. package/dist/adapters/StandardAdapter.js +203 -6
  30. package/dist/adapters/StandardAdapter.js.map +1 -1
  31. package/dist/adapters/X402Adapter.d.ts +208 -0
  32. package/dist/adapters/X402Adapter.d.ts.map +1 -0
  33. package/dist/adapters/X402Adapter.js +423 -0
  34. package/dist/adapters/X402Adapter.js.map +1 -0
  35. package/dist/adapters/index.d.ts +8 -0
  36. package/dist/adapters/index.d.ts.map +1 -1
  37. package/dist/adapters/index.js +19 -1
  38. package/dist/adapters/index.js.map +1 -1
  39. package/dist/cli/commands/init.d.ts +4 -0
  40. package/dist/cli/commands/init.d.ts.map +1 -1
  41. package/dist/cli/commands/init.js +146 -4
  42. package/dist/cli/commands/init.js.map +1 -1
  43. package/dist/config/networks.d.ts +9 -0
  44. package/dist/config/networks.d.ts.map +1 -1
  45. package/dist/config/networks.js +27 -12
  46. package/dist/config/networks.js.map +1 -1
  47. package/dist/erc8004/ERC8004Bridge.d.ts +155 -0
  48. package/dist/erc8004/ERC8004Bridge.d.ts.map +1 -0
  49. package/dist/erc8004/ERC8004Bridge.js +325 -0
  50. package/dist/erc8004/ERC8004Bridge.js.map +1 -0
  51. package/dist/erc8004/ReputationReporter.d.ts +223 -0
  52. package/dist/erc8004/ReputationReporter.d.ts.map +1 -0
  53. package/dist/erc8004/ReputationReporter.js +266 -0
  54. package/dist/erc8004/ReputationReporter.js.map +1 -0
  55. package/dist/erc8004/index.d.ts +36 -0
  56. package/dist/erc8004/index.d.ts.map +1 -0
  57. package/dist/erc8004/index.js +46 -0
  58. package/dist/erc8004/index.js.map +1 -0
  59. package/dist/index.d.ts +11 -1
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +51 -2
  62. package/dist/index.js.map +1 -1
  63. package/dist/level0/provide.d.ts.map +1 -1
  64. package/dist/level0/provide.js +2 -1
  65. package/dist/level0/provide.js.map +1 -1
  66. package/dist/level1/Agent.d.ts.map +1 -1
  67. package/dist/level1/Agent.js +11 -3
  68. package/dist/level1/Agent.js.map +1 -1
  69. package/dist/protocol/ACTPKernel.d.ts +1 -1
  70. package/dist/protocol/ACTPKernel.d.ts.map +1 -1
  71. package/dist/protocol/ACTPKernel.js +23 -12
  72. package/dist/protocol/ACTPKernel.js.map +1 -1
  73. package/dist/protocol/DIDResolver.js +1 -1
  74. package/dist/protocol/DIDResolver.js.map +1 -1
  75. package/dist/protocol/EASHelper.d.ts.map +1 -1
  76. package/dist/protocol/EASHelper.js +2 -3
  77. package/dist/protocol/EASHelper.js.map +1 -1
  78. package/dist/protocol/MessageSigner.d.ts.map +1 -1
  79. package/dist/protocol/MessageSigner.js +8 -8
  80. package/dist/protocol/MessageSigner.js.map +1 -1
  81. package/dist/runtime/BlockchainRuntime.d.ts +7 -0
  82. package/dist/runtime/BlockchainRuntime.d.ts.map +1 -1
  83. package/dist/runtime/BlockchainRuntime.js +40 -22
  84. package/dist/runtime/BlockchainRuntime.js.map +1 -1
  85. package/dist/runtime/IACTPRuntime.d.ts +21 -0
  86. package/dist/runtime/IACTPRuntime.d.ts.map +1 -1
  87. package/dist/runtime/MockRuntime.d.ts +19 -0
  88. package/dist/runtime/MockRuntime.d.ts.map +1 -1
  89. package/dist/runtime/MockRuntime.js +56 -4
  90. package/dist/runtime/MockRuntime.js.map +1 -1
  91. package/dist/runtime/types/MockState.d.ts +11 -2
  92. package/dist/runtime/types/MockState.d.ts.map +1 -1
  93. package/dist/runtime/types/MockState.js.map +1 -1
  94. package/dist/storage/ArchiveBundleBuilder.d.ts +150 -0
  95. package/dist/storage/ArchiveBundleBuilder.d.ts.map +1 -0
  96. package/dist/storage/ArchiveBundleBuilder.js +468 -0
  97. package/dist/storage/ArchiveBundleBuilder.js.map +1 -0
  98. package/dist/storage/ArweaveClient.d.ts +271 -0
  99. package/dist/storage/ArweaveClient.d.ts.map +1 -0
  100. package/dist/storage/ArweaveClient.js +761 -0
  101. package/dist/storage/ArweaveClient.js.map +1 -0
  102. package/dist/storage/FilebaseClient.d.ts +193 -0
  103. package/dist/storage/FilebaseClient.d.ts.map +1 -0
  104. package/dist/storage/FilebaseClient.js +643 -0
  105. package/dist/storage/FilebaseClient.js.map +1 -0
  106. package/dist/storage/index.d.ts +47 -0
  107. package/dist/storage/index.d.ts.map +1 -0
  108. package/dist/storage/index.js +64 -0
  109. package/dist/storage/index.js.map +1 -0
  110. package/dist/storage/types.d.ts +291 -0
  111. package/dist/storage/types.d.ts.map +1 -0
  112. package/dist/storage/types.js +18 -0
  113. package/dist/storage/types.js.map +1 -0
  114. package/dist/types/adapter.d.ts +359 -0
  115. package/dist/types/adapter.d.ts.map +1 -0
  116. package/dist/types/adapter.js +115 -0
  117. package/dist/types/adapter.js.map +1 -0
  118. package/dist/types/erc8004.d.ts +184 -0
  119. package/dist/types/erc8004.d.ts.map +1 -0
  120. package/dist/types/erc8004.js +132 -0
  121. package/dist/types/erc8004.js.map +1 -0
  122. package/dist/types/index.d.ts +3 -0
  123. package/dist/types/index.d.ts.map +1 -1
  124. package/dist/types/index.js +3 -0
  125. package/dist/types/index.js.map +1 -1
  126. package/dist/types/state.d.ts +5 -4
  127. package/dist/types/state.d.ts.map +1 -1
  128. package/dist/types/state.js +10 -9
  129. package/dist/types/state.js.map +1 -1
  130. package/dist/types/transaction.d.ts +12 -0
  131. package/dist/types/transaction.d.ts.map +1 -1
  132. package/dist/types/x402.d.ts +162 -0
  133. package/dist/types/x402.d.ts.map +1 -0
  134. package/dist/types/x402.js +162 -0
  135. package/dist/types/x402.js.map +1 -0
  136. package/dist/utils/IPFSClient.d.ts.map +1 -1
  137. package/dist/utils/IPFSClient.js +5 -2
  138. package/dist/utils/IPFSClient.js.map +1 -1
  139. package/dist/utils/NonceManager.d.ts.map +1 -1
  140. package/dist/utils/NonceManager.js +3 -2
  141. package/dist/utils/NonceManager.js.map +1 -1
  142. package/dist/utils/UsedAttestationTracker.d.ts.map +1 -1
  143. package/dist/utils/UsedAttestationTracker.js +3 -3
  144. package/dist/utils/UsedAttestationTracker.js.map +1 -1
  145. package/dist/utils/circuitBreaker.d.ts +136 -0
  146. package/dist/utils/circuitBreaker.d.ts.map +1 -0
  147. package/dist/utils/circuitBreaker.js +253 -0
  148. package/dist/utils/circuitBreaker.js.map +1 -0
  149. package/dist/utils/retry.d.ts +120 -0
  150. package/dist/utils/retry.d.ts.map +1 -0
  151. package/dist/utils/retry.js +260 -0
  152. package/dist/utils/retry.js.map +1 -0
  153. package/dist/utils/validation.d.ts +100 -0
  154. package/dist/utils/validation.d.ts.map +1 -1
  155. package/dist/utils/validation.js +248 -1
  156. package/dist/utils/validation.js.map +1 -1
  157. package/package.json +16 -3
  158. package/src/ACTPClient.ts +318 -2
  159. package/src/abi/ACTPKernel.json +16 -0
  160. package/src/adapters/AdapterRegistry.ts +173 -0
  161. package/src/adapters/AdapterRouter.ts +417 -0
  162. package/src/adapters/BaseAdapter.ts +25 -0
  163. package/src/adapters/BasicAdapter.ts +210 -3
  164. package/src/adapters/IAdapter.ts +292 -0
  165. package/src/adapters/StandardAdapter.ts +246 -7
  166. package/src/adapters/X402Adapter.ts +653 -0
  167. package/src/adapters/index.ts +27 -0
  168. package/src/cli/commands/init.ts +166 -3
  169. package/src/config/networks.ts +36 -12
  170. package/src/erc8004/ERC8004Bridge.ts +461 -0
  171. package/src/erc8004/ReputationReporter.ts +472 -0
  172. package/src/erc8004/index.ts +61 -0
  173. package/src/index.ts +97 -0
  174. package/src/level0/provide.ts +2 -1
  175. package/src/level1/Agent.ts +13 -3
  176. package/src/protocol/ACTPKernel.ts +33 -12
  177. package/src/protocol/DIDResolver.ts +1 -1
  178. package/src/protocol/EASHelper.ts +2 -5
  179. package/src/protocol/MessageSigner.ts +8 -14
  180. package/src/runtime/BlockchainRuntime.ts +41 -45
  181. package/src/runtime/IACTPRuntime.ts +22 -0
  182. package/src/runtime/MockRuntime.ts +58 -4
  183. package/src/runtime/types/MockState.ts +12 -2
  184. package/src/storage/ArchiveBundleBuilder.ts +563 -0
  185. package/src/storage/ArweaveClient.ts +945 -0
  186. package/src/storage/FilebaseClient.ts +790 -0
  187. package/src/storage/index.ts +96 -0
  188. package/src/storage/types.ts +348 -0
  189. package/src/types/adapter.ts +296 -0
  190. package/src/types/erc8004.ts +293 -0
  191. package/src/types/index.ts +3 -0
  192. package/src/types/state.ts +10 -9
  193. package/src/types/transaction.ts +12 -0
  194. package/src/types/x402.ts +219 -0
  195. package/src/utils/IPFSClient.ts +5 -4
  196. package/src/utils/NonceManager.ts +3 -2
  197. package/src/utils/UsedAttestationTracker.ts +3 -5
  198. package/src/utils/circuitBreaker.ts +324 -0
  199. package/src/utils/fsSafe.ts +5 -0
  200. package/src/utils/retry.ts +365 -0
  201. package/src/utils/validation.ts +295 -1
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Retry Utility - Exponential Backoff with Jitter (P1-2)
3
+ *
4
+ * Implements retry logic for storage operations with:
5
+ * - Exponential backoff (doubles delay each attempt)
6
+ * - Random jitter (prevents thundering herd)
7
+ * - Maximum retry limit
8
+ * - Configurable retry conditions
9
+ *
10
+ * @module utils/retry
11
+ */
12
+
13
+ import { StorageRateLimitError } from '../errors';
14
+
15
+ // ============================================================================
16
+ // Types
17
+ // ============================================================================
18
+
19
+ /**
20
+ * Retry configuration options
21
+ */
22
+ export interface RetryOptions {
23
+ /** Maximum number of retry attempts (default: 3) */
24
+ maxAttempts?: number;
25
+
26
+ /** Initial delay in milliseconds (default: 1000) */
27
+ initialDelayMs?: number;
28
+
29
+ /** Maximum delay in milliseconds (default: 30000) */
30
+ maxDelayMs?: number;
31
+
32
+ /** Multiplier for exponential backoff (default: 2) */
33
+ backoffMultiplier?: number;
34
+
35
+ /** Jitter factor 0-1 (default: 0.1 = ±10%) */
36
+ jitterFactor?: number;
37
+
38
+ /** Function to determine if error is retryable (default: built-in check) */
39
+ isRetryable?: (error: unknown) => boolean;
40
+
41
+ /** Callback for each retry attempt (for logging) */
42
+ onRetry?: (attempt: number, error: unknown, delayMs: number) => void;
43
+ }
44
+
45
+ /**
46
+ * Result of retry operation
47
+ */
48
+ export interface RetryResult<T> {
49
+ /** Operation result (if successful) */
50
+ result?: T;
51
+
52
+ /** Final error (if all retries failed) */
53
+ error?: unknown;
54
+
55
+ /** Number of attempts made */
56
+ attempts: number;
57
+
58
+ /** Total time spent (including delays) */
59
+ totalTimeMs: number;
60
+
61
+ /** Whether operation succeeded */
62
+ success: boolean;
63
+ }
64
+
65
+ // ============================================================================
66
+ // Constants
67
+ // ============================================================================
68
+
69
+ const DEFAULT_MAX_ATTEMPTS = 3;
70
+ const DEFAULT_INITIAL_DELAY_MS = 1000;
71
+ const DEFAULT_MAX_DELAY_MS = 10000; // Reduced from 30s to 10s (NEW-4: prevent long stalls)
72
+ const DEFAULT_BACKOFF_MULTIPLIER = 2;
73
+ const DEFAULT_JITTER_FACTOR = 0.1;
74
+
75
+ /**
76
+ * Default list of retryable HTTP status codes
77
+ */
78
+ const RETRYABLE_STATUS_CODES = new Set([
79
+ 408, // Request Timeout
80
+ 429, // Too Many Requests
81
+ 500, // Internal Server Error
82
+ 502, // Bad Gateway
83
+ 503, // Service Unavailable
84
+ 504 // Gateway Timeout
85
+ ]);
86
+
87
+ /**
88
+ * Default list of retryable error codes
89
+ */
90
+ const RETRYABLE_ERROR_CODES = new Set([
91
+ 'ECONNRESET',
92
+ 'ECONNREFUSED',
93
+ 'ETIMEDOUT',
94
+ 'ENOTFOUND',
95
+ 'ENETUNREACH',
96
+ 'EAI_AGAIN',
97
+ 'EPIPE',
98
+ 'EHOSTUNREACH'
99
+ ]);
100
+
101
+ // ============================================================================
102
+ // Functions
103
+ // ============================================================================
104
+
105
+ /**
106
+ * Default function to determine if error is retryable
107
+ *
108
+ * @param error - Error to check
109
+ * @returns True if error should trigger retry
110
+ */
111
+ export function isRetryableError(error: unknown): boolean {
112
+ if (!error) return false;
113
+
114
+ // Rate limit errors are always retryable
115
+ if (error instanceof StorageRateLimitError) {
116
+ return true;
117
+ }
118
+
119
+ const e = error as any;
120
+
121
+ // Check HTTP status codes
122
+ if (e.statusCode && RETRYABLE_STATUS_CODES.has(e.statusCode)) {
123
+ return true;
124
+ }
125
+
126
+ if (e.status && RETRYABLE_STATUS_CODES.has(e.status)) {
127
+ return true;
128
+ }
129
+
130
+ // Check error codes (network errors)
131
+ if (e.code && RETRYABLE_ERROR_CODES.has(e.code)) {
132
+ return true;
133
+ }
134
+
135
+ // Check for timeout errors
136
+ if (e.name === 'AbortError' || e.name === 'TimeoutError') {
137
+ return true;
138
+ }
139
+
140
+ // Check message for common retryable patterns
141
+ const message = String(e.message || e).toLowerCase();
142
+ if (
143
+ message.includes('timeout') ||
144
+ message.includes('rate limit') ||
145
+ message.includes('too many requests') ||
146
+ message.includes('service unavailable') ||
147
+ message.includes('temporarily unavailable')
148
+ ) {
149
+ return true;
150
+ }
151
+
152
+ return false;
153
+ }
154
+
155
+ /**
156
+ * Calculate delay with exponential backoff and jitter
157
+ *
158
+ * @param attempt - Current attempt number (1-based)
159
+ * @param options - Retry options
160
+ * @returns Delay in milliseconds
161
+ */
162
+ export function calculateBackoffDelay(
163
+ attempt: number,
164
+ options: RetryOptions = {}
165
+ ): number {
166
+ const {
167
+ initialDelayMs = DEFAULT_INITIAL_DELAY_MS,
168
+ maxDelayMs = DEFAULT_MAX_DELAY_MS,
169
+ backoffMultiplier = DEFAULT_BACKOFF_MULTIPLIER,
170
+ jitterFactor = DEFAULT_JITTER_FACTOR
171
+ } = options;
172
+
173
+ // Exponential backoff: initialDelay * (multiplier ^ (attempt - 1))
174
+ const exponentialDelay = initialDelayMs * Math.pow(backoffMultiplier, attempt - 1);
175
+
176
+ // Cap at max delay
177
+ const cappedDelay = Math.min(exponentialDelay, maxDelayMs);
178
+
179
+ // Add jitter: ±jitterFactor
180
+ const jitter = cappedDelay * jitterFactor * (Math.random() * 2 - 1);
181
+ const finalDelay = Math.max(0, cappedDelay + jitter);
182
+
183
+ return Math.round(finalDelay);
184
+ }
185
+
186
+ /**
187
+ * Sleep for specified duration
188
+ *
189
+ * @param ms - Milliseconds to sleep
190
+ */
191
+ function sleep(ms: number): Promise<void> {
192
+ return new Promise(resolve => setTimeout(resolve, ms));
193
+ }
194
+
195
+ /**
196
+ * Execute async operation with retry logic
197
+ *
198
+ * Implements exponential backoff with jitter for handling transient failures.
199
+ * Particularly useful for storage operations that may fail due to:
200
+ * - Rate limiting
201
+ * - Network timeouts
202
+ * - Temporary service unavailability
203
+ *
204
+ * @param operation - Async function to execute
205
+ * @param options - Retry configuration
206
+ * @returns Promise resolving to operation result or throwing final error
207
+ *
208
+ * @example
209
+ * ```typescript
210
+ * // Basic usage
211
+ * const result = await withRetry(
212
+ * () => client.uploadJSON(data),
213
+ * { maxAttempts: 3 }
214
+ * );
215
+ *
216
+ * // With logging
217
+ * const result = await withRetry(
218
+ * () => client.downloadJSON(cid),
219
+ * {
220
+ * maxAttempts: 5,
221
+ * onRetry: (attempt, error, delay) => {
222
+ * console.log(`Retry ${attempt}, waiting ${delay}ms:`, error);
223
+ * }
224
+ * }
225
+ * );
226
+ * ```
227
+ */
228
+ export async function withRetry<T>(
229
+ operation: () => Promise<T>,
230
+ options: RetryOptions = {}
231
+ ): Promise<T> {
232
+ const {
233
+ maxAttempts = DEFAULT_MAX_ATTEMPTS,
234
+ isRetryable = isRetryableError,
235
+ onRetry
236
+ } = options;
237
+
238
+ let lastError: unknown;
239
+ const startTime = Date.now();
240
+
241
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
242
+ try {
243
+ return await operation();
244
+ } catch (error) {
245
+ lastError = error;
246
+
247
+ // Check if this is the last attempt
248
+ if (attempt >= maxAttempts) {
249
+ break;
250
+ }
251
+
252
+ // Check if error is retryable
253
+ if (!isRetryable(error)) {
254
+ break;
255
+ }
256
+
257
+ // Calculate delay
258
+ let delayMs = calculateBackoffDelay(attempt, options);
259
+
260
+ // If rate limit error with retry-after header, use that instead
261
+ if (error instanceof StorageRateLimitError && error.details?.retryAfter) {
262
+ delayMs = Math.max(delayMs, error.details.retryAfter * 1000);
263
+ }
264
+
265
+ // Notify callback
266
+ if (onRetry) {
267
+ onRetry(attempt, error, delayMs);
268
+ }
269
+
270
+ // Wait before retry
271
+ await sleep(delayMs);
272
+ }
273
+ }
274
+
275
+ // All retries failed
276
+ throw lastError;
277
+ }
278
+
279
+ /**
280
+ * Execute async operation with retry logic, returning detailed result
281
+ *
282
+ * Unlike `withRetry`, this function never throws - it returns a result object
283
+ * with success/failure status and metadata.
284
+ *
285
+ * @param operation - Async function to execute
286
+ * @param options - Retry configuration
287
+ * @returns Promise resolving to detailed result object
288
+ *
289
+ * @example
290
+ * ```typescript
291
+ * const result = await withRetryResult(
292
+ * () => client.uploadJSON(data),
293
+ * { maxAttempts: 3 }
294
+ * );
295
+ *
296
+ * if (result.success) {
297
+ * console.log('Uploaded:', result.result);
298
+ * } else {
299
+ * console.error(`Failed after ${result.attempts} attempts:`, result.error);
300
+ * }
301
+ * ```
302
+ */
303
+ export async function withRetryResult<T>(
304
+ operation: () => Promise<T>,
305
+ options: RetryOptions = {}
306
+ ): Promise<RetryResult<T>> {
307
+ const {
308
+ maxAttempts = DEFAULT_MAX_ATTEMPTS,
309
+ isRetryable = isRetryableError,
310
+ onRetry
311
+ } = options;
312
+
313
+ let lastError: unknown;
314
+ const startTime = Date.now();
315
+ let attempts = 0;
316
+
317
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
318
+ attempts = attempt;
319
+
320
+ try {
321
+ const result = await operation();
322
+ return {
323
+ result,
324
+ attempts,
325
+ totalTimeMs: Date.now() - startTime,
326
+ success: true
327
+ };
328
+ } catch (error) {
329
+ lastError = error;
330
+
331
+ // Check if this is the last attempt
332
+ if (attempt >= maxAttempts) {
333
+ break;
334
+ }
335
+
336
+ // Check if error is retryable
337
+ if (!isRetryable(error)) {
338
+ break;
339
+ }
340
+
341
+ // Calculate delay
342
+ let delayMs = calculateBackoffDelay(attempt, options);
343
+
344
+ // If rate limit error with retry-after header, use that instead
345
+ if (error instanceof StorageRateLimitError && error.details?.retryAfter) {
346
+ delayMs = Math.max(delayMs, error.details.retryAfter * 1000);
347
+ }
348
+
349
+ // Notify callback
350
+ if (onRetry) {
351
+ onRetry(attempt, error, delayMs);
352
+ }
353
+
354
+ // Wait before retry
355
+ await sleep(delayMs);
356
+ }
357
+ }
358
+
359
+ return {
360
+ error: lastError,
361
+ attempts,
362
+ totalTimeMs: Date.now() - startTime,
363
+ success: false
364
+ };
365
+ }
@@ -2,13 +2,72 @@ import { isAddress, getAddress } from 'ethers';
2
2
  import {
3
3
  InvalidAddressError,
4
4
  InvalidAmountError,
5
- ValidationError
5
+ ValidationError,
6
+ InvalidCIDError,
7
+ InvalidArweaveTxIdError
6
8
  } from '../errors';
7
9
 
8
10
  /**
9
11
  * Input validation utilities
10
12
  */
11
13
 
14
+ // ============================================================================
15
+ // Shared Validation Patterns (AIP-7 Storage)
16
+ // ============================================================================
17
+
18
+ /** Ethereum address validation pattern */
19
+ export const ADDRESS_PATTERN = /^0x[a-fA-F0-9]{40}$/;
20
+
21
+ /** Transaction ID (bytes32) validation pattern */
22
+ export const TX_ID_PATTERN = /^0x[a-fA-F0-9]{64}$/;
23
+
24
+ /** Hash (bytes32) validation pattern */
25
+ export const HASH_PATTERN = /^0x[a-fA-F0-9]{64}$/;
26
+
27
+ /** Signature (65 bytes = 130 hex chars) validation pattern */
28
+ export const SIGNATURE_PATTERN = /^0x[a-fA-F0-9]{130}$/;
29
+
30
+ /** CID validation pattern (CIDv0 or CIDv1) */
31
+ export const CID_PATTERN = /^(Qm[1-9A-HJ-NP-Za-km-z]{44}|b[a-z2-7]{58,})$/;
32
+
33
+ /** Arweave TX ID pattern (43 characters, base64url) */
34
+ export const ARWEAVE_TX_ID_PATTERN = /^[a-zA-Z0-9_-]{43}$/;
35
+
36
+ /** Semver pattern for version validation */
37
+ export const SEMVER_PATTERN = /^\d+\.\d+\.\d+$/;
38
+
39
+ // ============================================================================
40
+ // Gateway URL Whitelist (SSRF Protection - P0-1)
41
+ // ============================================================================
42
+
43
+ /**
44
+ * Allowed IPFS gateway domains
45
+ * Only whitelisted gateways are allowed for downloads
46
+ */
47
+ export const ALLOWED_IPFS_GATEWAYS = [
48
+ 'ipfs.filebase.io',
49
+ 'gateway.pinata.cloud',
50
+ 'cloudflare-ipfs.com',
51
+ 'ipfs.io',
52
+ 'dweb.link',
53
+ 'w3s.link',
54
+ 'nftstorage.link'
55
+ ] as const;
56
+
57
+ /**
58
+ * Allowed Arweave gateway domains
59
+ * Only whitelisted gateways are allowed for downloads
60
+ */
61
+ export const ALLOWED_ARWEAVE_GATEWAYS = [
62
+ 'arweave.net',
63
+ 'gateway.irys.xyz',
64
+ 'arweave.dev'
65
+ ] as const;
66
+
67
+ // ============================================================================
68
+ // Validation Functions
69
+ // ============================================================================
70
+
12
71
  /**
13
72
  * Validate Ethereum address
14
73
  */
@@ -113,13 +172,19 @@ function isPrivateIP(ip: string): boolean {
113
172
  }
114
173
 
115
174
  // IPv6 patterns (without brackets)
175
+ // Includes both standard ::ffff: and alternative ::ffff:0: notation (NEW-2 fix)
116
176
  const ipv6PrivatePatterns = [
117
177
  /^::1$/, // IPv6 loopback
118
178
  /^::ffff:127\./, // IPv4-mapped localhost
179
+ /^::ffff:0:127\./, // Alternative IPv4-mapped localhost
119
180
  /^::ffff:10\./, // IPv4-mapped private 10.x
181
+ /^::ffff:0:10\./, // Alternative IPv4-mapped private 10.x
120
182
  /^::ffff:192\.168\./, // IPv4-mapped private 192.168.x
183
+ /^::ffff:0:192\.168\./, // Alternative IPv4-mapped private 192.168.x
121
184
  /^::ffff:172\.(1[6-9]|2\d|3[01])\./, // IPv4-mapped private 172.16-31.x
185
+ /^::ffff:0:172\.(1[6-9]|2\d|3[01])\./,// Alternative IPv4-mapped private 172.16-31.x
122
186
  /^::ffff:169\.254\./, // IPv4-mapped link-local (CRITICAL: AWS metadata)
187
+ /^::ffff:0:169\.254\./, // Alternative IPv4-mapped link-local
123
188
  /^fc00:/i, // IPv6 ULA fc00::/7
124
189
  /^fd/i, // IPv6 ULA fd00::/8
125
190
  /^fe80:/i // IPv6 link-local fe80::/10
@@ -244,3 +309,232 @@ export async function validateEndpointURL(endpoint: string, fieldName: string =
244
309
  // IPFS endpoints skip DNS check (no DNS resolution for IPFS CIDs)
245
310
  }
246
311
 
312
+ // ============================================================================
313
+ // Storage Validation Functions (AIP-7)
314
+ // ============================================================================
315
+
316
+ /**
317
+ * Validate IPFS CID format
318
+ *
319
+ * @param cid - IPFS CID to validate
320
+ * @param fieldName - Field name for error messages
321
+ * @throws {InvalidCIDError} If CID is invalid
322
+ */
323
+ export function validateCID(cid: string, fieldName: string = 'cid'): void {
324
+ if (!cid || typeof cid !== 'string') {
325
+ throw new InvalidCIDError(String(cid), 'CID is required');
326
+ }
327
+
328
+ if (!CID_PATTERN.test(cid)) {
329
+ throw new InvalidCIDError(cid, 'Invalid CID format (expected CIDv0 Qm... or CIDv1 bafy...)');
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Validate Arweave transaction ID format
335
+ *
336
+ * @param txId - Arweave TX ID to validate
337
+ * @param fieldName - Field name for error messages
338
+ * @throws {InvalidArweaveTxIdError} If TX ID is invalid
339
+ */
340
+ export function validateArweaveTxId(txId: string, fieldName: string = 'txId'): void {
341
+ if (!txId || typeof txId !== 'string') {
342
+ throw new InvalidArweaveTxIdError(String(txId), 'TX ID is required');
343
+ }
344
+
345
+ if (!ARWEAVE_TX_ID_PATTERN.test(txId)) {
346
+ throw new InvalidArweaveTxIdError(
347
+ txId,
348
+ 'Invalid format (expected 43 character base64url string)'
349
+ );
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Validate gateway URL against whitelist (SSRF Protection - P0-1)
355
+ *
356
+ * SECURITY FIX: Only allow downloads from whitelisted gateway domains.
357
+ * This prevents SSRF attacks where attacker controls the gateway URL.
358
+ *
359
+ * @param url - Full gateway URL to validate
360
+ * @param allowedGateways - List of allowed gateway domains
361
+ * @param fieldName - Field name for error messages
362
+ * @throws {ValidationError} If gateway is not whitelisted
363
+ */
364
+ export function validateGatewayURL(
365
+ url: string,
366
+ allowedGateways: readonly string[],
367
+ fieldName: string = 'gatewayUrl'
368
+ ): void {
369
+ if (!url || typeof url !== 'string') {
370
+ throw new ValidationError(fieldName, 'Gateway URL is required');
371
+ }
372
+
373
+ let parsedUrl: URL;
374
+ try {
375
+ parsedUrl = new URL(url);
376
+ } catch {
377
+ throw new ValidationError(fieldName, 'Invalid URL format');
378
+ }
379
+
380
+ // Must be HTTPS
381
+ if (parsedUrl.protocol !== 'https:') {
382
+ throw new ValidationError(fieldName, 'Gateway URL must use HTTPS');
383
+ }
384
+
385
+ // NEW-3: Validate port (must be 443 or default, prevents port bypass attacks)
386
+ const port = parsedUrl.port;
387
+ if (port && !['443', ''].includes(port)) {
388
+ throw new ValidationError(
389
+ fieldName,
390
+ `Gateway URL must use standard HTTPS port (443). Found port: ${port}. ` +
391
+ `Non-standard ports may bypass whitelist intent.`
392
+ );
393
+ }
394
+
395
+ // Check hostname against whitelist
396
+ const hostname = parsedUrl.hostname.toLowerCase();
397
+ const isAllowed = allowedGateways.some(gateway =>
398
+ hostname === gateway.toLowerCase() ||
399
+ hostname.endsWith('.' + gateway.toLowerCase())
400
+ );
401
+
402
+ if (!isAllowed) {
403
+ throw new ValidationError(
404
+ fieldName,
405
+ `Gateway "${hostname}" is not in the allowed list. ` +
406
+ `Allowed gateways: ${allowedGateways.join(', ')}. ` +
407
+ `This restriction prevents SSRF attacks.`
408
+ );
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Validate semver version string
414
+ *
415
+ * @param version - Version string to validate
416
+ * @param fieldName - Field name for error messages
417
+ * @throws {ValidationError} If version is invalid
418
+ */
419
+ export function validateSemver(version: string, fieldName: string = 'version'): void {
420
+ if (!version || typeof version !== 'string') {
421
+ throw new ValidationError(fieldName, 'Version is required');
422
+ }
423
+
424
+ if (!SEMVER_PATTERN.test(version)) {
425
+ throw new ValidationError(fieldName, 'Must be semver format (e.g., 1.0.0)');
426
+ }
427
+ }
428
+
429
+ /**
430
+ * Validate hash (bytes32) format
431
+ *
432
+ * @param hash - Hash to validate
433
+ * @param fieldName - Field name for error messages
434
+ * @throws {ValidationError} If hash is invalid
435
+ */
436
+ export function validateHash(hash: string, fieldName: string = 'hash'): void {
437
+ if (!hash || typeof hash !== 'string') {
438
+ throw new ValidationError(fieldName, 'Hash is required');
439
+ }
440
+
441
+ if (!HASH_PATTERN.test(hash)) {
442
+ throw new ValidationError(fieldName, 'Invalid hash format (expected bytes32 hex string)');
443
+ }
444
+ }
445
+
446
+ /**
447
+ * Validate signature format (65 bytes)
448
+ *
449
+ * @param signature - Signature to validate
450
+ * @param fieldName - Field name for error messages
451
+ * @throws {ValidationError} If signature is invalid
452
+ */
453
+ export function validateSignature(signature: string, fieldName: string = 'signature'): void {
454
+ if (!signature || typeof signature !== 'string') {
455
+ throw new ValidationError(fieldName, 'Signature is required');
456
+ }
457
+
458
+ if (!SIGNATURE_PATTERN.test(signature)) {
459
+ throw new ValidationError(
460
+ fieldName,
461
+ 'Invalid signature format (expected 65 bytes = 0x + 130 hex chars)'
462
+ );
463
+ }
464
+ }
465
+
466
+ // ============================================================================
467
+ // Error Sanitization (P0-2)
468
+ // ============================================================================
469
+
470
+ /**
471
+ * Sanitize error messages to remove sensitive data
472
+ *
473
+ * SECURITY FIX (P0-2): Removes credentials, private keys, and other
474
+ * sensitive data from error messages before logging/returning.
475
+ *
476
+ * @param error - Error to sanitize
477
+ * @returns Sanitized error message
478
+ */
479
+ export function sanitizeErrorMessage(error: unknown): string {
480
+ if (!error) return 'Unknown error';
481
+
482
+ let message = '';
483
+ if (error instanceof Error) {
484
+ message = error.message;
485
+ } else if (typeof error === 'string') {
486
+ message = error;
487
+ } else {
488
+ message = String(error);
489
+ }
490
+
491
+ // Patterns to redact
492
+ const sensitivePatterns = [
493
+ // Private keys (hex)
494
+ /0x[a-fA-F0-9]{64}/g,
495
+ // AWS access key IDs
496
+ /AKIA[0-9A-Z]{16}/g,
497
+ // AWS secret keys (40 chars)
498
+ /[a-zA-Z0-9/+=]{40}/g,
499
+ // Bearer tokens
500
+ /Bearer\s+[a-zA-Z0-9._-]+/gi,
501
+ // API keys (generic pattern)
502
+ /api[_-]?key[=:]\s*["']?[a-zA-Z0-9_-]+["']?/gi,
503
+ // Secret in URL query params
504
+ /secret[=][^&\s]+/gi,
505
+ // Password in URL
506
+ /password[=][^&\s]+/gi,
507
+ // Authorization headers
508
+ /authorization[=:]\s*["']?[^"'\s]+["']?/gi
509
+ ];
510
+
511
+ let sanitized = message;
512
+ for (const pattern of sensitivePatterns) {
513
+ sanitized = sanitized.replace(pattern, '[REDACTED]');
514
+ }
515
+
516
+ return sanitized;
517
+ }
518
+
519
+ /**
520
+ * Create a safe error object for external consumption
521
+ *
522
+ * SECURITY FIX (P0-2): Returns error without stack trace or sensitive details
523
+ *
524
+ * @param error - Original error
525
+ * @param operation - What operation failed
526
+ * @returns Safe error object
527
+ */
528
+ export function createSafeError(
529
+ error: unknown,
530
+ operation: string
531
+ ): { message: string; code: string; operation: string } {
532
+ const sanitizedMessage = sanitizeErrorMessage(error);
533
+
534
+ // Don't expose internal details - generic message with operation context
535
+ return {
536
+ message: `Operation failed: ${operation}. ${sanitizedMessage}`,
537
+ code: (error as any)?.code || 'UNKNOWN_ERROR',
538
+ operation
539
+ };
540
+ }