@agirails/sdk 2.5.3 → 2.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/dist/ACTPClient.d.ts +18 -0
  2. package/dist/ACTPClient.d.ts.map +1 -1
  3. package/dist/ACTPClient.js +72 -23
  4. package/dist/ACTPClient.js.map +1 -1
  5. package/dist/adapters/BasicAdapter.d.ts +15 -0
  6. package/dist/adapters/BasicAdapter.d.ts.map +1 -1
  7. package/dist/adapters/BasicAdapter.js +33 -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 +90 -12
  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/config/networks.d.ts +10 -2
  20. package/dist/config/networks.d.ts.map +1 -1
  21. package/dist/config/networks.js +31 -22
  22. package/dist/config/networks.js.map +1 -1
  23. package/dist/level0/request.d.ts.map +1 -1
  24. package/dist/level0/request.js +2 -1
  25. package/dist/level0/request.js.map +1 -1
  26. package/dist/runtime/BlockchainRuntime.d.ts.map +1 -1
  27. package/dist/runtime/BlockchainRuntime.js +11 -5
  28. package/dist/runtime/BlockchainRuntime.js.map +1 -1
  29. package/dist/utils/IPFSClient.d.ts +3 -1
  30. package/dist/utils/IPFSClient.d.ts.map +1 -1
  31. package/dist/utils/IPFSClient.js +27 -7
  32. package/dist/utils/IPFSClient.js.map +1 -1
  33. package/dist/wallet/AutoWalletProvider.d.ts +11 -1
  34. package/dist/wallet/AutoWalletProvider.d.ts.map +1 -1
  35. package/dist/wallet/AutoWalletProvider.js +84 -19
  36. package/dist/wallet/AutoWalletProvider.js.map +1 -1
  37. package/dist/wallet/IWalletProvider.d.ts +34 -0
  38. package/dist/wallet/IWalletProvider.d.ts.map +1 -1
  39. package/dist/wallet/SmartWalletRouter.d.ts +128 -0
  40. package/dist/wallet/SmartWalletRouter.d.ts.map +1 -0
  41. package/dist/wallet/SmartWalletRouter.js +248 -0
  42. package/dist/wallet/SmartWalletRouter.js.map +1 -0
  43. package/dist/wallet/aa/DualNonceManager.d.ts +26 -1
  44. package/dist/wallet/aa/DualNonceManager.d.ts.map +1 -1
  45. package/dist/wallet/aa/DualNonceManager.js +140 -6
  46. package/dist/wallet/aa/DualNonceManager.js.map +1 -1
  47. package/package.json +3 -6
  48. package/src/ACTPClient.ts +0 -1579
  49. package/src/abi/ACTPKernel.json +0 -1356
  50. package/src/abi/AgentRegistry.json +0 -915
  51. package/src/abi/ERC20.json +0 -40
  52. package/src/abi/EscrowVault.json +0 -134
  53. package/src/abi/IdentityRegistry.json +0 -316
  54. package/src/adapters/AdapterRegistry.ts +0 -173
  55. package/src/adapters/AdapterRouter.ts +0 -416
  56. package/src/adapters/BaseAdapter.ts +0 -498
  57. package/src/adapters/BasicAdapter.ts +0 -514
  58. package/src/adapters/IAdapter.ts +0 -292
  59. package/src/adapters/StandardAdapter.ts +0 -555
  60. package/src/adapters/X402Adapter.ts +0 -731
  61. package/src/adapters/index.ts +0 -60
  62. package/src/builders/DeliveryProofBuilder.ts +0 -327
  63. package/src/builders/QuoteBuilder.ts +0 -483
  64. package/src/builders/index.ts +0 -17
  65. package/src/cli/commands/balance.ts +0 -110
  66. package/src/cli/commands/batch.ts +0 -487
  67. package/src/cli/commands/config.ts +0 -231
  68. package/src/cli/commands/deploy-check.ts +0 -364
  69. package/src/cli/commands/deploy-env.ts +0 -120
  70. package/src/cli/commands/diff.ts +0 -141
  71. package/src/cli/commands/init.ts +0 -469
  72. package/src/cli/commands/mint.ts +0 -116
  73. package/src/cli/commands/pay.ts +0 -113
  74. package/src/cli/commands/publish.ts +0 -475
  75. package/src/cli/commands/pull.ts +0 -124
  76. package/src/cli/commands/register.ts +0 -247
  77. package/src/cli/commands/simulate.ts +0 -345
  78. package/src/cli/commands/time.ts +0 -302
  79. package/src/cli/commands/tx.ts +0 -448
  80. package/src/cli/commands/watch.ts +0 -211
  81. package/src/cli/index.ts +0 -134
  82. package/src/cli/utils/client.ts +0 -252
  83. package/src/cli/utils/config.ts +0 -389
  84. package/src/cli/utils/output.ts +0 -465
  85. package/src/cli/utils/wallet.ts +0 -109
  86. package/src/config/agirailsmd.ts +0 -262
  87. package/src/config/networks.ts +0 -275
  88. package/src/config/pendingPublish.ts +0 -237
  89. package/src/config/publishPipeline.ts +0 -359
  90. package/src/config/syncOperations.ts +0 -279
  91. package/src/erc8004/ERC8004Bridge.ts +0 -462
  92. package/src/erc8004/ReputationReporter.ts +0 -468
  93. package/src/erc8004/index.ts +0 -61
  94. package/src/errors/index.ts +0 -427
  95. package/src/index.ts +0 -364
  96. package/src/level0/Provider.ts +0 -117
  97. package/src/level0/ServiceDirectory.ts +0 -131
  98. package/src/level0/index.ts +0 -10
  99. package/src/level0/provide.ts +0 -132
  100. package/src/level0/request.ts +0 -432
  101. package/src/level1/Agent.ts +0 -1426
  102. package/src/level1/index.ts +0 -10
  103. package/src/level1/pricing/PriceCalculator.ts +0 -255
  104. package/src/level1/pricing/PricingStrategy.ts +0 -198
  105. package/src/level1/types/Job.ts +0 -179
  106. package/src/level1/types/Options.ts +0 -291
  107. package/src/level1/types/index.ts +0 -8
  108. package/src/protocol/ACTPKernel.ts +0 -808
  109. package/src/protocol/AgentRegistry.ts +0 -559
  110. package/src/protocol/DIDManager.ts +0 -629
  111. package/src/protocol/DIDResolver.ts +0 -554
  112. package/src/protocol/EASHelper.ts +0 -378
  113. package/src/protocol/EscrowVault.ts +0 -255
  114. package/src/protocol/EventMonitor.ts +0 -204
  115. package/src/protocol/MessageSigner.ts +0 -510
  116. package/src/protocol/ProofGenerator.ts +0 -339
  117. package/src/protocol/QuoteBuilder.ts +0 -15
  118. package/src/registry/AgentRegistryClient.ts +0 -202
  119. package/src/runtime/BlockchainRuntime.ts +0 -1015
  120. package/src/runtime/IACTPRuntime.ts +0 -306
  121. package/src/runtime/MockRuntime.ts +0 -1298
  122. package/src/runtime/MockStateManager.ts +0 -577
  123. package/src/runtime/index.ts +0 -25
  124. package/src/runtime/types/MockState.ts +0 -237
  125. package/src/storage/ArchiveBundleBuilder.ts +0 -561
  126. package/src/storage/ArweaveClient.ts +0 -946
  127. package/src/storage/FilebaseClient.ts +0 -790
  128. package/src/storage/index.ts +0 -96
  129. package/src/storage/types.ts +0 -348
  130. package/src/types/adapter.ts +0 -310
  131. package/src/types/agent.ts +0 -79
  132. package/src/types/did.ts +0 -223
  133. package/src/types/eip712.ts +0 -175
  134. package/src/types/erc8004.ts +0 -293
  135. package/src/types/escrow.ts +0 -27
  136. package/src/types/index.ts +0 -17
  137. package/src/types/message.ts +0 -145
  138. package/src/types/state.ts +0 -87
  139. package/src/types/transaction.ts +0 -69
  140. package/src/types/x402.ts +0 -251
  141. package/src/utils/ErrorRecoveryGuide.ts +0 -676
  142. package/src/utils/Helpers.ts +0 -688
  143. package/src/utils/IPFSClient.ts +0 -368
  144. package/src/utils/Logger.ts +0 -484
  145. package/src/utils/NonceManager.ts +0 -591
  146. package/src/utils/RateLimiter.ts +0 -534
  147. package/src/utils/ReceivedNonceTracker.ts +0 -567
  148. package/src/utils/SDKLifecycle.ts +0 -416
  149. package/src/utils/SecureNonce.ts +0 -78
  150. package/src/utils/Semaphore.ts +0 -276
  151. package/src/utils/UsedAttestationTracker.ts +0 -385
  152. package/src/utils/canonicalJson.ts +0 -38
  153. package/src/utils/circuitBreaker.ts +0 -324
  154. package/src/utils/computeTypeHash.ts +0 -48
  155. package/src/utils/fsSafe.ts +0 -80
  156. package/src/utils/index.ts +0 -80
  157. package/src/utils/retry.ts +0 -364
  158. package/src/utils/security.ts +0 -418
  159. package/src/utils/validation.ts +0 -540
  160. package/src/wallet/AutoWalletProvider.ts +0 -299
  161. package/src/wallet/EOAWalletProvider.ts +0 -69
  162. package/src/wallet/IWalletProvider.ts +0 -135
  163. package/src/wallet/aa/BundlerClient.ts +0 -274
  164. package/src/wallet/aa/DualNonceManager.ts +0 -173
  165. package/src/wallet/aa/PaymasterClient.ts +0 -174
  166. package/src/wallet/aa/TransactionBatcher.ts +0 -353
  167. package/src/wallet/aa/UserOpBuilder.ts +0 -246
  168. package/src/wallet/aa/constants.ts +0 -60
  169. 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
- }