@agirails/sdk 2.5.2 → 2.5.4

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 (172) hide show
  1. package/dist/ACTPClient.d.ts +18 -0
  2. package/dist/ACTPClient.d.ts.map +1 -1
  3. package/dist/ACTPClient.js +67 -22
  4. package/dist/ACTPClient.js.map +1 -1
  5. package/dist/adapters/BasicAdapter.d.ts +12 -0
  6. package/dist/adapters/BasicAdapter.d.ts.map +1 -1
  7. package/dist/adapters/BasicAdapter.js +30 -4
  8. package/dist/adapters/BasicAdapter.js.map +1 -1
  9. package/dist/adapters/StandardAdapter.d.ts +20 -3
  10. package/dist/adapters/StandardAdapter.d.ts.map +1 -1
  11. package/dist/adapters/StandardAdapter.js +45 -11
  12. package/dist/adapters/StandardAdapter.js.map +1 -1
  13. package/dist/cli/commands/publish.js +16 -4
  14. package/dist/cli/commands/publish.js.map +1 -1
  15. package/dist/cli/commands/register.js +16 -4
  16. package/dist/cli/commands/register.js.map +1 -1
  17. package/dist/cli/commands/tx.js +31 -3
  18. package/dist/cli/commands/tx.js.map +1 -1
  19. package/dist/cli/utils/client.d.ts.map +1 -1
  20. package/dist/cli/utils/client.js +1 -0
  21. package/dist/cli/utils/client.js.map +1 -1
  22. package/dist/config/networks.d.ts +2 -2
  23. package/dist/config/networks.d.ts.map +1 -1
  24. package/dist/config/networks.js +27 -22
  25. package/dist/config/networks.js.map +1 -1
  26. package/dist/level0/request.d.ts.map +1 -1
  27. package/dist/level0/request.js +2 -1
  28. package/dist/level0/request.js.map +1 -1
  29. package/dist/runtime/BlockchainRuntime.d.ts.map +1 -1
  30. package/dist/runtime/BlockchainRuntime.js +11 -5
  31. package/dist/runtime/BlockchainRuntime.js.map +1 -1
  32. package/dist/runtime/MockStateManager.d.ts.map +1 -1
  33. package/dist/runtime/MockStateManager.js +2 -1
  34. package/dist/runtime/MockStateManager.js.map +1 -1
  35. package/dist/utils/IPFSClient.d.ts +3 -1
  36. package/dist/utils/IPFSClient.d.ts.map +1 -1
  37. package/dist/utils/IPFSClient.js +27 -7
  38. package/dist/utils/IPFSClient.js.map +1 -1
  39. package/dist/wallet/AutoWalletProvider.d.ts.map +1 -1
  40. package/dist/wallet/AutoWalletProvider.js +52 -18
  41. package/dist/wallet/AutoWalletProvider.js.map +1 -1
  42. package/dist/wallet/SmartWalletRouter.d.ts +116 -0
  43. package/dist/wallet/SmartWalletRouter.d.ts.map +1 -0
  44. package/dist/wallet/SmartWalletRouter.js +212 -0
  45. package/dist/wallet/SmartWalletRouter.js.map +1 -0
  46. package/dist/wallet/aa/DualNonceManager.d.ts +19 -0
  47. package/dist/wallet/aa/DualNonceManager.d.ts.map +1 -1
  48. package/dist/wallet/aa/DualNonceManager.js +100 -5
  49. package/dist/wallet/aa/DualNonceManager.js.map +1 -1
  50. package/package.json +3 -6
  51. package/src/ACTPClient.ts +0 -1579
  52. package/src/abi/ACTPKernel.json +0 -1356
  53. package/src/abi/AgentRegistry.json +0 -915
  54. package/src/abi/ERC20.json +0 -40
  55. package/src/abi/EscrowVault.json +0 -134
  56. package/src/abi/IdentityRegistry.json +0 -316
  57. package/src/adapters/AdapterRegistry.ts +0 -173
  58. package/src/adapters/AdapterRouter.ts +0 -416
  59. package/src/adapters/BaseAdapter.ts +0 -498
  60. package/src/adapters/BasicAdapter.ts +0 -514
  61. package/src/adapters/IAdapter.ts +0 -292
  62. package/src/adapters/StandardAdapter.ts +0 -555
  63. package/src/adapters/X402Adapter.ts +0 -731
  64. package/src/adapters/index.ts +0 -60
  65. package/src/builders/DeliveryProofBuilder.ts +0 -327
  66. package/src/builders/QuoteBuilder.ts +0 -483
  67. package/src/builders/index.ts +0 -17
  68. package/src/cli/commands/balance.ts +0 -110
  69. package/src/cli/commands/batch.ts +0 -487
  70. package/src/cli/commands/config.ts +0 -231
  71. package/src/cli/commands/deploy-check.ts +0 -364
  72. package/src/cli/commands/deploy-env.ts +0 -120
  73. package/src/cli/commands/diff.ts +0 -141
  74. package/src/cli/commands/init.ts +0 -469
  75. package/src/cli/commands/mint.ts +0 -116
  76. package/src/cli/commands/pay.ts +0 -113
  77. package/src/cli/commands/publish.ts +0 -475
  78. package/src/cli/commands/pull.ts +0 -124
  79. package/src/cli/commands/register.ts +0 -247
  80. package/src/cli/commands/simulate.ts +0 -345
  81. package/src/cli/commands/time.ts +0 -302
  82. package/src/cli/commands/tx.ts +0 -448
  83. package/src/cli/commands/watch.ts +0 -211
  84. package/src/cli/index.ts +0 -134
  85. package/src/cli/utils/client.ts +0 -251
  86. package/src/cli/utils/config.ts +0 -389
  87. package/src/cli/utils/output.ts +0 -465
  88. package/src/cli/utils/wallet.ts +0 -109
  89. package/src/config/agirailsmd.ts +0 -262
  90. package/src/config/networks.ts +0 -275
  91. package/src/config/pendingPublish.ts +0 -237
  92. package/src/config/publishPipeline.ts +0 -359
  93. package/src/config/syncOperations.ts +0 -279
  94. package/src/erc8004/ERC8004Bridge.ts +0 -462
  95. package/src/erc8004/ReputationReporter.ts +0 -468
  96. package/src/erc8004/index.ts +0 -61
  97. package/src/errors/index.ts +0 -427
  98. package/src/index.ts +0 -364
  99. package/src/level0/Provider.ts +0 -117
  100. package/src/level0/ServiceDirectory.ts +0 -131
  101. package/src/level0/index.ts +0 -10
  102. package/src/level0/provide.ts +0 -132
  103. package/src/level0/request.ts +0 -432
  104. package/src/level1/Agent.ts +0 -1426
  105. package/src/level1/index.ts +0 -10
  106. package/src/level1/pricing/PriceCalculator.ts +0 -255
  107. package/src/level1/pricing/PricingStrategy.ts +0 -198
  108. package/src/level1/types/Job.ts +0 -179
  109. package/src/level1/types/Options.ts +0 -291
  110. package/src/level1/types/index.ts +0 -8
  111. package/src/protocol/ACTPKernel.ts +0 -808
  112. package/src/protocol/AgentRegistry.ts +0 -559
  113. package/src/protocol/DIDManager.ts +0 -629
  114. package/src/protocol/DIDResolver.ts +0 -554
  115. package/src/protocol/EASHelper.ts +0 -378
  116. package/src/protocol/EscrowVault.ts +0 -255
  117. package/src/protocol/EventMonitor.ts +0 -204
  118. package/src/protocol/MessageSigner.ts +0 -510
  119. package/src/protocol/ProofGenerator.ts +0 -339
  120. package/src/protocol/QuoteBuilder.ts +0 -15
  121. package/src/registry/AgentRegistryClient.ts +0 -202
  122. package/src/runtime/BlockchainRuntime.ts +0 -1015
  123. package/src/runtime/IACTPRuntime.ts +0 -306
  124. package/src/runtime/MockRuntime.ts +0 -1298
  125. package/src/runtime/MockStateManager.ts +0 -576
  126. package/src/runtime/index.ts +0 -25
  127. package/src/runtime/types/MockState.ts +0 -237
  128. package/src/storage/ArchiveBundleBuilder.ts +0 -561
  129. package/src/storage/ArweaveClient.ts +0 -946
  130. package/src/storage/FilebaseClient.ts +0 -790
  131. package/src/storage/index.ts +0 -96
  132. package/src/storage/types.ts +0 -348
  133. package/src/types/adapter.ts +0 -310
  134. package/src/types/agent.ts +0 -79
  135. package/src/types/did.ts +0 -223
  136. package/src/types/eip712.ts +0 -175
  137. package/src/types/erc8004.ts +0 -293
  138. package/src/types/escrow.ts +0 -27
  139. package/src/types/index.ts +0 -17
  140. package/src/types/message.ts +0 -145
  141. package/src/types/state.ts +0 -87
  142. package/src/types/transaction.ts +0 -69
  143. package/src/types/x402.ts +0 -251
  144. package/src/utils/ErrorRecoveryGuide.ts +0 -676
  145. package/src/utils/Helpers.ts +0 -688
  146. package/src/utils/IPFSClient.ts +0 -368
  147. package/src/utils/Logger.ts +0 -484
  148. package/src/utils/NonceManager.ts +0 -591
  149. package/src/utils/RateLimiter.ts +0 -534
  150. package/src/utils/ReceivedNonceTracker.ts +0 -567
  151. package/src/utils/SDKLifecycle.ts +0 -416
  152. package/src/utils/SecureNonce.ts +0 -78
  153. package/src/utils/Semaphore.ts +0 -276
  154. package/src/utils/UsedAttestationTracker.ts +0 -385
  155. package/src/utils/canonicalJson.ts +0 -38
  156. package/src/utils/circuitBreaker.ts +0 -324
  157. package/src/utils/computeTypeHash.ts +0 -48
  158. package/src/utils/fsSafe.ts +0 -80
  159. package/src/utils/index.ts +0 -80
  160. package/src/utils/retry.ts +0 -364
  161. package/src/utils/security.ts +0 -418
  162. package/src/utils/validation.ts +0 -540
  163. package/src/wallet/AutoWalletProvider.ts +0 -299
  164. package/src/wallet/EOAWalletProvider.ts +0 -69
  165. package/src/wallet/IWalletProvider.ts +0 -135
  166. package/src/wallet/aa/BundlerClient.ts +0 -274
  167. package/src/wallet/aa/DualNonceManager.ts +0 -173
  168. package/src/wallet/aa/PaymasterClient.ts +0 -174
  169. package/src/wallet/aa/TransactionBatcher.ts +0 -353
  170. package/src/wallet/aa/UserOpBuilder.ts +0 -246
  171. package/src/wallet/aa/constants.ts +0 -60
  172. package/src/wallet/keystore.ts +0 -240
@@ -1,790 +0,0 @@
1
- /**
2
- * FilebaseClient - IPFS Storage via Filebase S3 API (AIP-7 §4.2)
3
- *
4
- * Provides S3-compatible IPFS pinning via Filebase.
5
- * Used for hot storage of request metadata and delivery proofs.
6
- *
7
- * Security Features (Post-Audit):
8
- * - Gateway URL whitelist (SSRF protection)
9
- * - Download size limits (DoS protection)
10
- * - Credential sanitization in errors
11
- * - Retry with exponential backoff
12
- * - Circuit breaker for gateway health tracking
13
- *
14
- * @module storage/FilebaseClient
15
- */
16
-
17
- import { S3Client, PutObjectCommand, HeadObjectCommand } from '@aws-sdk/client-s3';
18
- import {
19
- StorageError,
20
- StorageAuthenticationError,
21
- StorageRateLimitError,
22
- UploadTimeoutError,
23
- DownloadTimeoutError,
24
- ContentNotFoundError,
25
- FileSizeLimitExceededError,
26
- ValidationError
27
- } from '../errors';
28
- import {
29
- FilebaseConfig,
30
- IPFSUploadResult,
31
- DownloadResult
32
- } from './types';
33
- import {
34
- validateCID,
35
- validateGatewayURL,
36
- sanitizeErrorMessage,
37
- ALLOWED_IPFS_GATEWAYS
38
- } from '../utils/validation';
39
- import { withRetry, RetryOptions } from '../utils/retry';
40
- import {
41
- GatewayCircuitBreaker,
42
- } from '../utils/circuitBreaker';
43
-
44
- // ============================================================================
45
- // Constants
46
- // ============================================================================
47
-
48
- const DEFAULT_ENDPOINT = 'https://s3.filebase.com';
49
- const DEFAULT_GATEWAY = 'https://ipfs.filebase.io/ipfs/';
50
- const DEFAULT_BUCKET = 'agirails-storage';
51
- const DEFAULT_TIMEOUT = 30000; // 30 seconds
52
- const DEFAULT_MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB
53
- const DEFAULT_MAX_DOWNLOAD_SIZE = 50 * 1024 * 1024; // 50MB (P1-1: DoS protection)
54
- const DEFAULT_REGION = 'us-east-1';
55
-
56
- // ============================================================================
57
- // FilebaseClient Class
58
- // ============================================================================
59
-
60
- /**
61
- * FilebaseClient - IPFS storage via Filebase S3 API
62
- *
63
- * Provides hot storage for:
64
- * - AIP-1 request metadata
65
- * - AIP-4 delivery proofs
66
- * - Service descriptors
67
- *
68
- * @example
69
- * ```typescript
70
- * const client = new FilebaseClient({
71
- * accessKey: process.env.FILEBASE_ACCESS_KEY!,
72
- * secretKey: process.env.FILEBASE_SECRET_KEY!,
73
- * bucket: 'my-agirails-bucket'
74
- * });
75
- *
76
- * // Upload JSON
77
- * const result = await client.uploadJSON({ message: 'Hello IPFS!' });
78
- * console.log('CID:', result.cid);
79
- *
80
- * // Download JSON
81
- * const data = await client.downloadJSON(result.cid);
82
- * ```
83
- */
84
- export class FilebaseClient {
85
- private readonly s3: S3Client;
86
- private readonly bucket: string;
87
- private readonly gatewayUrl: string;
88
- private readonly timeout: number;
89
- private readonly maxFileSize: number;
90
- private readonly maxDownloadSize: number;
91
- private readonly retryOptions: RetryOptions;
92
- private readonly circuitBreaker: GatewayCircuitBreaker | null;
93
- private readonly circuitBreakerEnabled: boolean;
94
-
95
- /**
96
- * Create a new FilebaseClient
97
- *
98
- * @param config - Filebase configuration
99
- * @throws {ValidationError} If required config is missing or gateway URL not whitelisted
100
- */
101
- constructor(config: FilebaseConfig) {
102
- // Validate required config
103
- if (!config.accessKey) {
104
- throw new ValidationError('accessKey', 'Filebase access key is required');
105
- }
106
- if (!config.secretKey) {
107
- throw new ValidationError('secretKey', 'Filebase secret key is required');
108
- }
109
-
110
- // P0-1: Validate gateway URL against whitelist (SSRF protection)
111
- const gatewayUrl = config.gatewayUrl || DEFAULT_GATEWAY;
112
- validateGatewayURL(gatewayUrl, ALLOWED_IPFS_GATEWAYS, 'gatewayUrl');
113
-
114
- // Initialize S3 client
115
- this.s3 = new S3Client({
116
- endpoint: config.endpoint || DEFAULT_ENDPOINT,
117
- region: DEFAULT_REGION,
118
- credentials: {
119
- accessKeyId: config.accessKey,
120
- secretAccessKey: config.secretKey
121
- },
122
- forcePathStyle: true // Required for S3-compatible services
123
- });
124
-
125
- this.bucket = config.bucket || DEFAULT_BUCKET;
126
- this.gatewayUrl = gatewayUrl;
127
- this.timeout = config.timeout || DEFAULT_TIMEOUT;
128
- this.maxFileSize = config.maxFileSize || DEFAULT_MAX_FILE_SIZE;
129
- this.maxDownloadSize = config.maxDownloadSize || DEFAULT_MAX_DOWNLOAD_SIZE;
130
-
131
- // P1-2: Default retry options
132
- this.retryOptions = {
133
- maxAttempts: 3,
134
- initialDelayMs: 1000,
135
- maxDelayMs: 10000,
136
- backoffMultiplier: 2
137
- };
138
-
139
- // Circuit breaker for gateway health tracking
140
- this.circuitBreakerEnabled = config.circuitBreaker?.enabled !== false;
141
- if (this.circuitBreakerEnabled) {
142
- this.circuitBreaker = new GatewayCircuitBreaker({
143
- failureThreshold: config.circuitBreaker?.failureThreshold,
144
- resetTimeoutMs: config.circuitBreaker?.resetTimeoutMs,
145
- failureWindowMs: config.circuitBreaker?.failureWindowMs,
146
- successThreshold: config.circuitBreaker?.successThreshold
147
- });
148
- } else {
149
- this.circuitBreaker = null;
150
- }
151
- }
152
-
153
- // ==========================================================================
154
- // Public Methods
155
- // ==========================================================================
156
-
157
- /**
158
- * Upload JSON to IPFS via Filebase (automatic pinning)
159
- *
160
- * @param data - JSON-serializable data to upload
161
- * @param options - Upload options
162
- * @returns Upload result with CID
163
- * @throws {FileSizeLimitExceededError} If data exceeds max size
164
- * @throws {StorageAuthenticationError} If credentials are invalid
165
- * @throws {UploadTimeoutError} If upload times out
166
- * @throws {StorageError} If upload fails
167
- *
168
- * @example
169
- * ```typescript
170
- * const result = await client.uploadJSON({
171
- * version: '1.0.0',
172
- * serviceType: 'text-generation',
173
- * inputData: { prompt: 'Hello!' }
174
- * });
175
- * console.log('Uploaded to IPFS:', result.cid);
176
- * ```
177
- */
178
- async uploadJSON(
179
- data: unknown,
180
- options?: { key?: string; metadata?: Record<string, string> }
181
- ): Promise<IPFSUploadResult> {
182
- // Serialize to JSON
183
- const jsonString = JSON.stringify(data);
184
- const buffer = Buffer.from(jsonString, 'utf-8');
185
-
186
- // Check file size
187
- if (buffer.length > this.maxFileSize) {
188
- throw new FileSizeLimitExceededError(buffer.length, this.maxFileSize);
189
- }
190
-
191
- // Generate unique key if not provided
192
- const key = options?.key || this.generateKey('.json');
193
-
194
- try {
195
- // Upload to Filebase (auto-pins to IPFS)
196
- const command = new PutObjectCommand({
197
- Bucket: this.bucket,
198
- Key: key,
199
- Body: buffer,
200
- ContentType: 'application/json',
201
- Metadata: options?.metadata
202
- });
203
-
204
- const response = await this.withTimeout(
205
- this.s3.send(command),
206
- this.timeout,
207
- 'upload'
208
- );
209
-
210
- // Extract CID from response headers
211
- // Filebase returns CID in x-amz-meta-cid header
212
- const cid = (response as any).$metadata?.httpStatusCode === 200
213
- ? await this.getCIDFromKey(key)
214
- : undefined;
215
-
216
- if (!cid) {
217
- throw new StorageError('upload', 'Failed to retrieve CID after upload');
218
- }
219
-
220
- return {
221
- cid,
222
- size: buffer.length,
223
- uploadedAt: new Date()
224
- };
225
- } catch (error: any) {
226
- // Handle specific errors
227
- if (error instanceof FileSizeLimitExceededError) throw error;
228
- if (error instanceof UploadTimeoutError) throw error;
229
-
230
- if (error.name === 'CredentialsProviderError' || error.Code === 'InvalidAccessKeyId') {
231
- throw new StorageAuthenticationError('Filebase');
232
- }
233
-
234
- if (error.Code === 'SlowDown' || error.statusCode === 429) {
235
- const retryAfter = error.$metadata?.httpHeaders?.['retry-after'];
236
- throw new StorageRateLimitError(retryAfter ? parseInt(retryAfter, 10) : undefined);
237
- }
238
-
239
- // P0-2: Sanitize error message (may contain AWS credentials)
240
- throw new StorageError('upload', sanitizeErrorMessage(error), {
241
- originalError: error.name,
242
- key
243
- });
244
- }
245
- }
246
-
247
- /**
248
- * Download JSON from IPFS via gateway
249
- *
250
- * Security Features:
251
- * - Size limit enforcement (P1-1: DoS protection)
252
- * - Retry with exponential backoff (P1-2)
253
- * - Centralized CID validation (P1-3)
254
- * - Credential sanitization in errors (P0-2)
255
- *
256
- * @param cid - IPFS CID to download
257
- * @returns Downloaded and parsed JSON data
258
- * @throws {InvalidCIDError} If CID format is invalid
259
- * @throws {ContentNotFoundError} If content not found
260
- * @throws {FileSizeLimitExceededError} If content exceeds size limit
261
- * @throws {DownloadTimeoutError} If download times out
262
- * @throws {StorageError} If download fails
263
- *
264
- * @example
265
- * ```typescript
266
- * const data = await client.downloadJSON('bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi');
267
- * console.log('Downloaded:', data);
268
- * ```
269
- */
270
- async downloadJSON<T = unknown>(cid: string): Promise<DownloadResult<T>> {
271
- // P1-3: Use centralized CID validation
272
- validateCID(cid);
273
-
274
- const url = `${this.gatewayUrl}${cid}`;
275
-
276
- // Circuit breaker: check gateway health before attempting download
277
- if (this.circuitBreaker && !this.circuitBreaker.isHealthy(this.gatewayUrl)) {
278
- const state = this.circuitBreaker.getState(this.gatewayUrl);
279
- const failures = this.circuitBreaker.getFailureCount(this.gatewayUrl);
280
- throw new StorageError(
281
- 'download',
282
- `Gateway circuit breaker OPEN for ${this.gatewayUrl}. ` +
283
- `State: ${state}, Failures: ${failures}. ` +
284
- `Please wait for cooldown period before retrying.`,
285
- { cid, circuitState: state }
286
- );
287
- }
288
-
289
- // P1-2: Wrap in retry logic
290
- return withRetry(
291
- async () => {
292
- const controller = new AbortController();
293
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
294
-
295
- try {
296
- const response = await fetch(url, {
297
- signal: controller.signal,
298
- headers: {
299
- 'Accept': 'application/json'
300
- }
301
- });
302
-
303
- clearTimeout(timeoutId);
304
-
305
- if (!response.ok) {
306
- if (response.status === 404) {
307
- throw new ContentNotFoundError(cid);
308
- }
309
- throw new StorageError('download', `HTTP ${response.status}: ${response.statusText}`, { cid });
310
- }
311
-
312
- // P1-1: Check Content-Length header before downloading
313
- const contentLength = response.headers.get('content-length');
314
- if (contentLength && parseInt(contentLength, 10) > this.maxDownloadSize) {
315
- throw new FileSizeLimitExceededError(
316
- parseInt(contentLength, 10),
317
- this.maxDownloadSize
318
- );
319
- }
320
-
321
- // P1-1: Stream response with size limit enforcement
322
- const reader = response.body?.getReader();
323
- if (!reader) {
324
- throw new StorageError('download', 'No response body', { cid });
325
- }
326
-
327
- const chunks: Uint8Array[] = [];
328
- let totalSize = 0;
329
-
330
- // eslint-disable-next-line no-constant-condition
331
- while (true) {
332
- const { done, value } = await reader.read();
333
- if (done) break;
334
-
335
- totalSize += value.length;
336
-
337
- // P1-1: Enforce size limit during streaming
338
- if (totalSize > this.maxDownloadSize) {
339
- reader.cancel();
340
- throw new FileSizeLimitExceededError(totalSize, this.maxDownloadSize);
341
- }
342
-
343
- chunks.push(value);
344
- }
345
-
346
- // Combine chunks into text
347
- const decoder = new TextDecoder();
348
- const text = chunks.map(chunk => decoder.decode(chunk, { stream: true })).join('') +
349
- decoder.decode();
350
-
351
- const data = JSON.parse(text) as T;
352
-
353
- // Circuit breaker: record success
354
- if (this.circuitBreaker) {
355
- this.circuitBreaker.recordSuccess(this.gatewayUrl);
356
- }
357
-
358
- return {
359
- data,
360
- size: totalSize,
361
- downloadedAt: new Date()
362
- };
363
- } catch (error: any) {
364
- clearTimeout(timeoutId);
365
-
366
- // Circuit breaker: record failure for retryable errors
367
- // (non-retryable errors like 404 shouldn't count as gateway failures)
368
- if (this.circuitBreaker && this.isGatewayFailure(error)) {
369
- this.circuitBreaker.recordFailure(this.gatewayUrl);
370
- }
371
-
372
- if (error instanceof ContentNotFoundError) throw error;
373
- if (error instanceof FileSizeLimitExceededError) throw error;
374
-
375
- if (error.name === 'AbortError') {
376
- throw new DownloadTimeoutError(cid, this.timeout);
377
- }
378
-
379
- if (error instanceof SyntaxError) {
380
- throw new StorageError('download', `Invalid JSON content for CID: ${cid}`, { cid });
381
- }
382
-
383
- // P0-2: Sanitize error message
384
- throw new StorageError('download', sanitizeErrorMessage(error), {
385
- cid,
386
- originalError: error.name
387
- });
388
- }
389
- },
390
- this.retryOptions
391
- );
392
- }
393
-
394
- /**
395
- * Upload binary data to IPFS
396
- *
397
- * @param data - Binary data to upload
398
- * @param contentType - MIME type of the content
399
- * @param options - Upload options
400
- * @returns Upload result with CID
401
- */
402
- async uploadBinary(
403
- data: Buffer | Uint8Array,
404
- contentType: string,
405
- options?: { key?: string; metadata?: Record<string, string> }
406
- ): Promise<IPFSUploadResult> {
407
- const buffer = Buffer.from(data);
408
-
409
- // Check file size
410
- if (buffer.length > this.maxFileSize) {
411
- throw new FileSizeLimitExceededError(buffer.length, this.maxFileSize);
412
- }
413
-
414
- // Generate key with appropriate extension
415
- const extension = this.getExtensionFromContentType(contentType);
416
- const key = options?.key || this.generateKey(extension);
417
-
418
- try {
419
- const command = new PutObjectCommand({
420
- Bucket: this.bucket,
421
- Key: key,
422
- Body: buffer,
423
- ContentType: contentType,
424
- Metadata: options?.metadata
425
- });
426
-
427
- await this.withTimeout(this.s3.send(command), this.timeout, 'upload');
428
-
429
- const cid = await this.getCIDFromKey(key);
430
- if (!cid) {
431
- throw new StorageError('upload', 'Failed to retrieve CID after upload');
432
- }
433
-
434
- return {
435
- cid,
436
- size: buffer.length,
437
- uploadedAt: new Date()
438
- };
439
- } catch (error: any) {
440
- if (error instanceof FileSizeLimitExceededError) throw error;
441
- if (error instanceof UploadTimeoutError) throw error;
442
- if (error instanceof StorageError) throw error;
443
-
444
- // P0-2: Sanitize error message (may contain AWS credentials)
445
- throw new StorageError('upload', sanitizeErrorMessage(error), {
446
- originalError: error.name,
447
- key
448
- });
449
- }
450
- }
451
-
452
- /**
453
- * Download binary data from IPFS
454
- *
455
- * Security Features:
456
- * - Size limit enforcement (P1-1: DoS protection)
457
- * - Retry with exponential backoff (P1-2)
458
- * - Centralized CID validation (P1-3)
459
- *
460
- * @param cid - IPFS CID to download
461
- * @returns Downloaded binary data
462
- */
463
- async downloadBinary(cid: string): Promise<DownloadResult<Buffer>> {
464
- // P1-3: Use centralized CID validation
465
- validateCID(cid);
466
-
467
- const url = `${this.gatewayUrl}${cid}`;
468
-
469
- // Circuit breaker: check gateway health before attempting download
470
- if (this.circuitBreaker && !this.circuitBreaker.isHealthy(this.gatewayUrl)) {
471
- const state = this.circuitBreaker.getState(this.gatewayUrl);
472
- const failures = this.circuitBreaker.getFailureCount(this.gatewayUrl);
473
- throw new StorageError(
474
- 'download',
475
- `Gateway circuit breaker OPEN for ${this.gatewayUrl}. ` +
476
- `State: ${state}, Failures: ${failures}. ` +
477
- `Please wait for cooldown period before retrying.`,
478
- { cid, circuitState: state }
479
- );
480
- }
481
-
482
- // P1-2: Wrap in retry logic
483
- return withRetry(
484
- async () => {
485
- const controller = new AbortController();
486
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
487
-
488
- try {
489
- const response = await fetch(url, { signal: controller.signal });
490
-
491
- clearTimeout(timeoutId);
492
-
493
- if (!response.ok) {
494
- if (response.status === 404) {
495
- throw new ContentNotFoundError(cid);
496
- }
497
- throw new StorageError('download', `HTTP ${response.status}: ${response.statusText}`, { cid });
498
- }
499
-
500
- // P1-1: Check Content-Length header before downloading
501
- const contentLength = response.headers.get('content-length');
502
- if (contentLength && parseInt(contentLength, 10) > this.maxDownloadSize) {
503
- throw new FileSizeLimitExceededError(
504
- parseInt(contentLength, 10),
505
- this.maxDownloadSize
506
- );
507
- }
508
-
509
- // P1-1: Stream response with size limit enforcement
510
- const reader = response.body?.getReader();
511
- if (!reader) {
512
- throw new StorageError('download', 'No response body', { cid });
513
- }
514
-
515
- const chunks: Uint8Array[] = [];
516
- let totalSize = 0;
517
-
518
- // eslint-disable-next-line no-constant-condition
519
- while (true) {
520
- const { done, value } = await reader.read();
521
- if (done) break;
522
-
523
- totalSize += value.length;
524
-
525
- // P1-1: Enforce size limit during streaming
526
- if (totalSize > this.maxDownloadSize) {
527
- reader.cancel();
528
- throw new FileSizeLimitExceededError(totalSize, this.maxDownloadSize);
529
- }
530
-
531
- chunks.push(value);
532
- }
533
-
534
- // Combine chunks into Buffer
535
- const buffer = Buffer.concat(chunks);
536
-
537
- // Circuit breaker: record success
538
- if (this.circuitBreaker) {
539
- this.circuitBreaker.recordSuccess(this.gatewayUrl);
540
- }
541
-
542
- return {
543
- data: buffer,
544
- size: buffer.length,
545
- downloadedAt: new Date()
546
- };
547
- } catch (error: any) {
548
- clearTimeout(timeoutId);
549
-
550
- // Circuit breaker: record failure for retryable errors
551
- if (this.circuitBreaker && this.isGatewayFailure(error)) {
552
- this.circuitBreaker.recordFailure(this.gatewayUrl);
553
- }
554
-
555
- if (error instanceof ContentNotFoundError) throw error;
556
- if (error instanceof FileSizeLimitExceededError) throw error;
557
-
558
- if (error.name === 'AbortError') {
559
- throw new DownloadTimeoutError(cid, this.timeout);
560
- }
561
-
562
- // P0-2: Sanitize error message
563
- throw new StorageError('download', sanitizeErrorMessage(error), {
564
- cid,
565
- originalError: error.name
566
- });
567
- }
568
- },
569
- this.retryOptions
570
- );
571
- }
572
-
573
- /**
574
- * Check if content exists on IPFS
575
- *
576
- * @param cid - IPFS CID to check
577
- * @returns True if content exists
578
- */
579
- async exists(cid: string): Promise<boolean> {
580
- // P1-3: Use centralized CID validation
581
- validateCID(cid);
582
-
583
- const url = `${this.gatewayUrl}${cid}`;
584
-
585
- try {
586
- const controller = new AbortController();
587
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
588
-
589
- const response = await fetch(url, {
590
- method: 'HEAD',
591
- signal: controller.signal
592
- });
593
-
594
- clearTimeout(timeoutId);
595
-
596
- return response.ok;
597
- } catch {
598
- return false;
599
- }
600
- }
601
-
602
- /**
603
- * Get the bucket name
604
- */
605
- getBucket(): string {
606
- return this.bucket;
607
- }
608
-
609
- /**
610
- * Get the gateway URL
611
- */
612
- getGatewayUrl(): string {
613
- return this.gatewayUrl;
614
- }
615
-
616
- // ==========================================================================
617
- // Private Methods
618
- // ==========================================================================
619
-
620
- /**
621
- * Generate unique key for S3 object
622
- */
623
- private generateKey(extension: string): string {
624
- const timestamp = Date.now();
625
- const random = Math.random().toString(36).substring(2, 10);
626
- return `${timestamp}-${random}${extension}`;
627
- }
628
-
629
- /**
630
- * Get CID from uploaded object via HeadObject
631
- * Filebase returns CID in x-amz-meta-cid header
632
- */
633
- private async getCIDFromKey(key: string): Promise<string | undefined> {
634
- try {
635
- const command = new HeadObjectCommand({
636
- Bucket: this.bucket,
637
- Key: key
638
- });
639
-
640
- const response = await this.s3.send(command);
641
-
642
- // Filebase stores CID in metadata
643
- return response.Metadata?.cid;
644
- } catch {
645
- return undefined;
646
- }
647
- }
648
-
649
-
650
- /**
651
- * Get file extension from content type
652
- */
653
- private getExtensionFromContentType(contentType: string): string {
654
- const types: Record<string, string> = {
655
- 'application/json': '.json',
656
- 'text/plain': '.txt',
657
- 'image/png': '.png',
658
- 'image/jpeg': '.jpg',
659
- 'application/pdf': '.pdf',
660
- 'application/octet-stream': '.bin'
661
- };
662
-
663
- return types[contentType] || '.bin';
664
- }
665
-
666
- /**
667
- * Wrap promise with timeout
668
- */
669
- private async withTimeout<T>(
670
- promise: Promise<T>,
671
- timeoutMs: number,
672
- operation: 'upload' | 'download'
673
- ): Promise<T> {
674
- let timeoutId: NodeJS.Timeout;
675
-
676
- const timeoutPromise = new Promise<never>((_, reject) => {
677
- timeoutId = setTimeout(() => {
678
- if (operation === 'upload') {
679
- reject(new UploadTimeoutError(timeoutMs));
680
- } else {
681
- reject(new DownloadTimeoutError('unknown', timeoutMs));
682
- }
683
- }, timeoutMs);
684
- });
685
-
686
- try {
687
- return await Promise.race([promise, timeoutPromise]);
688
- } finally {
689
- clearTimeout(timeoutId!);
690
- }
691
- }
692
-
693
- /**
694
- * Determine if an error represents a gateway failure
695
- * (should be counted by circuit breaker)
696
- *
697
- * Gateway failures are network/server errors that indicate
698
- * the gateway is unhealthy. Content-specific errors (404, invalid JSON)
699
- * are NOT gateway failures.
700
- *
701
- * @param error - Error to check
702
- * @returns true if error is a gateway failure
703
- */
704
- private isGatewayFailure(error: unknown): boolean {
705
- if (!error) return false;
706
-
707
- const e = error as any;
708
-
709
- // 404 Not Found - content doesn't exist, not a gateway failure
710
- if (error instanceof ContentNotFoundError) return false;
711
-
712
- // File size exceeded - content too large, not a gateway failure
713
- if (error instanceof FileSizeLimitExceededError) return false;
714
-
715
- // Invalid JSON - content is corrupted, not a gateway failure
716
- if (error instanceof SyntaxError) return false;
717
-
718
- // Timeout - gateway is slow/unresponsive, IS a failure
719
- if (e.name === 'AbortError' || e.name === 'TimeoutError') return true;
720
-
721
- // 5xx server errors - gateway issue
722
- if (e.status >= 500 && e.status < 600) return true;
723
- if (e.statusCode >= 500 && e.statusCode < 600) return true;
724
-
725
- // Network errors - gateway unreachable
726
- const networkErrors = ['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'ENOTFOUND'];
727
- if (e.code && networkErrors.includes(e.code)) return true;
728
-
729
- // Rate limiting - gateway overloaded
730
- if (e.status === 429 || e.statusCode === 429) return true;
731
-
732
- // Generic StorageError from network issues or HTTP errors
733
- if (error instanceof StorageError) {
734
- const message = e.message?.toLowerCase() || '';
735
- // Network-related errors
736
- if (message.includes('timeout') ||
737
- message.includes('network') ||
738
- message.includes('connection')) {
739
- return true;
740
- }
741
- // HTTP 5xx errors in message (e.g., "HTTP 500: Internal Server Error")
742
- if (/http 5\d{2}/.test(message)) {
743
- return true;
744
- }
745
- // HTTP 429 rate limiting in message
746
- if (/http 429/.test(message)) {
747
- return true;
748
- }
749
- }
750
-
751
- return false;
752
- }
753
-
754
- // ==========================================================================
755
- // Circuit Breaker Status (Public API)
756
- // ==========================================================================
757
-
758
- /**
759
- * Get circuit breaker status for the gateway
760
- *
761
- * @returns Circuit breaker status or null if disabled
762
- */
763
- getCircuitBreakerStatus(): {
764
- enabled: boolean;
765
- state: 'CLOSED' | 'OPEN' | 'HALF_OPEN';
766
- failures: number;
767
- isHealthy: boolean;
768
- } | null {
769
- if (!this.circuitBreaker) {
770
- return null;
771
- }
772
-
773
- return {
774
- enabled: true,
775
- state: this.circuitBreaker.getState(this.gatewayUrl),
776
- failures: this.circuitBreaker.getFailureCount(this.gatewayUrl),
777
- isHealthy: this.circuitBreaker.isHealthy(this.gatewayUrl)
778
- };
779
- }
780
-
781
- /**
782
- * Reset circuit breaker for the gateway
783
- * Use this to manually reset after a known temporary outage
784
- */
785
- resetCircuitBreaker(): void {
786
- if (this.circuitBreaker) {
787
- this.circuitBreaker.reset(this.gatewayUrl);
788
- }
789
- }
790
- }