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