@agirails/sdk 2.0.4 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +536 -87
  2. package/dist/adapters/BasicAdapter.d.ts.map +1 -1
  3. package/dist/adapters/BasicAdapter.js +8 -0
  4. package/dist/adapters/BasicAdapter.js.map +1 -1
  5. package/dist/adapters/StandardAdapter.d.ts +10 -5
  6. package/dist/adapters/StandardAdapter.d.ts.map +1 -1
  7. package/dist/adapters/StandardAdapter.js +19 -6
  8. package/dist/adapters/StandardAdapter.js.map +1 -1
  9. package/dist/config/networks.d.ts +9 -0
  10. package/dist/config/networks.d.ts.map +1 -1
  11. package/dist/config/networks.js +25 -10
  12. package/dist/config/networks.js.map +1 -1
  13. package/dist/index.d.ts +6 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +31 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/level0/provide.d.ts.map +1 -1
  18. package/dist/level0/provide.js +2 -1
  19. package/dist/level0/provide.js.map +1 -1
  20. package/dist/level1/Agent.d.ts.map +1 -1
  21. package/dist/level1/Agent.js +11 -3
  22. package/dist/level1/Agent.js.map +1 -1
  23. package/dist/protocol/ACTPKernel.d.ts.map +1 -1
  24. package/dist/protocol/ACTPKernel.js +7 -5
  25. package/dist/protocol/ACTPKernel.js.map +1 -1
  26. package/dist/protocol/DIDResolver.js +1 -1
  27. package/dist/protocol/DIDResolver.js.map +1 -1
  28. package/dist/protocol/EASHelper.d.ts.map +1 -1
  29. package/dist/protocol/EASHelper.js +2 -3
  30. package/dist/protocol/EASHelper.js.map +1 -1
  31. package/dist/protocol/MessageSigner.d.ts.map +1 -1
  32. package/dist/protocol/MessageSigner.js +8 -8
  33. package/dist/protocol/MessageSigner.js.map +1 -1
  34. package/dist/runtime/BlockchainRuntime.d.ts +7 -0
  35. package/dist/runtime/BlockchainRuntime.d.ts.map +1 -1
  36. package/dist/runtime/BlockchainRuntime.js +38 -22
  37. package/dist/runtime/BlockchainRuntime.js.map +1 -1
  38. package/dist/runtime/IACTPRuntime.d.ts +15 -0
  39. package/dist/runtime/IACTPRuntime.d.ts.map +1 -1
  40. package/dist/runtime/MockRuntime.d.ts +7 -0
  41. package/dist/runtime/MockRuntime.d.ts.map +1 -1
  42. package/dist/runtime/MockRuntime.js +15 -4
  43. package/dist/runtime/MockRuntime.js.map +1 -1
  44. package/dist/runtime/types/MockState.d.ts +5 -2
  45. package/dist/runtime/types/MockState.d.ts.map +1 -1
  46. package/dist/runtime/types/MockState.js.map +1 -1
  47. package/dist/storage/ArchiveBundleBuilder.d.ts +150 -0
  48. package/dist/storage/ArchiveBundleBuilder.d.ts.map +1 -0
  49. package/dist/storage/ArchiveBundleBuilder.js +468 -0
  50. package/dist/storage/ArchiveBundleBuilder.js.map +1 -0
  51. package/dist/storage/ArweaveClient.d.ts +271 -0
  52. package/dist/storage/ArweaveClient.d.ts.map +1 -0
  53. package/dist/storage/ArweaveClient.js +761 -0
  54. package/dist/storage/ArweaveClient.js.map +1 -0
  55. package/dist/storage/FilebaseClient.d.ts +193 -0
  56. package/dist/storage/FilebaseClient.d.ts.map +1 -0
  57. package/dist/storage/FilebaseClient.js +643 -0
  58. package/dist/storage/FilebaseClient.js.map +1 -0
  59. package/dist/storage/index.d.ts +47 -0
  60. package/dist/storage/index.d.ts.map +1 -0
  61. package/dist/storage/index.js +64 -0
  62. package/dist/storage/index.js.map +1 -0
  63. package/dist/storage/types.d.ts +291 -0
  64. package/dist/storage/types.d.ts.map +1 -0
  65. package/dist/storage/types.js +18 -0
  66. package/dist/storage/types.js.map +1 -0
  67. package/dist/types/state.d.ts +5 -4
  68. package/dist/types/state.d.ts.map +1 -1
  69. package/dist/types/state.js +10 -9
  70. package/dist/types/state.js.map +1 -1
  71. package/dist/utils/IPFSClient.d.ts.map +1 -1
  72. package/dist/utils/IPFSClient.js +5 -2
  73. package/dist/utils/IPFSClient.js.map +1 -1
  74. package/dist/utils/NonceManager.d.ts.map +1 -1
  75. package/dist/utils/NonceManager.js +3 -2
  76. package/dist/utils/NonceManager.js.map +1 -1
  77. package/dist/utils/UsedAttestationTracker.d.ts.map +1 -1
  78. package/dist/utils/UsedAttestationTracker.js +3 -3
  79. package/dist/utils/UsedAttestationTracker.js.map +1 -1
  80. package/dist/utils/circuitBreaker.d.ts +136 -0
  81. package/dist/utils/circuitBreaker.d.ts.map +1 -0
  82. package/dist/utils/circuitBreaker.js +253 -0
  83. package/dist/utils/circuitBreaker.js.map +1 -0
  84. package/dist/utils/retry.d.ts +120 -0
  85. package/dist/utils/retry.d.ts.map +1 -0
  86. package/dist/utils/retry.js +260 -0
  87. package/dist/utils/retry.js.map +1 -0
  88. package/dist/utils/validation.d.ts +100 -0
  89. package/dist/utils/validation.d.ts.map +1 -1
  90. package/dist/utils/validation.js +248 -1
  91. package/dist/utils/validation.js.map +1 -1
  92. package/package.json +14 -2
  93. package/src/adapters/BasicAdapter.ts +11 -0
  94. package/src/adapters/StandardAdapter.ts +26 -6
  95. package/src/config/networks.ts +34 -10
  96. package/src/index.ts +54 -0
  97. package/src/level0/provide.ts +2 -1
  98. package/src/level1/Agent.ts +13 -3
  99. package/src/protocol/ACTPKernel.ts +7 -5
  100. package/src/protocol/DIDResolver.ts +1 -1
  101. package/src/protocol/EASHelper.ts +2 -5
  102. package/src/protocol/MessageSigner.ts +8 -14
  103. package/src/runtime/BlockchainRuntime.ts +39 -45
  104. package/src/runtime/IACTPRuntime.ts +16 -0
  105. package/src/runtime/MockRuntime.ts +16 -4
  106. package/src/runtime/types/MockState.ts +5 -2
  107. package/src/storage/ArchiveBundleBuilder.ts +563 -0
  108. package/src/storage/ArweaveClient.ts +945 -0
  109. package/src/storage/FilebaseClient.ts +790 -0
  110. package/src/storage/index.ts +96 -0
  111. package/src/storage/types.ts +348 -0
  112. package/src/types/state.ts +10 -9
  113. package/src/utils/IPFSClient.ts +5 -4
  114. package/src/utils/NonceManager.ts +3 -2
  115. package/src/utils/UsedAttestationTracker.ts +3 -5
  116. package/src/utils/circuitBreaker.ts +324 -0
  117. package/src/utils/fsSafe.ts +5 -0
  118. package/src/utils/retry.ts +365 -0
  119. package/src/utils/validation.ts +295 -1
@@ -0,0 +1,761 @@
1
+ "use strict";
2
+ /**
3
+ * ArweaveClient - Permanent Storage via Irys (AIP-7 §4.3)
4
+ *
5
+ * Provides permanent storage on Arweave via Irys (formerly Bundlr).
6
+ * Used for archiving settled transaction bundles for compliance.
7
+ *
8
+ * Security Features (Post-Audit):
9
+ * - Gateway URL whitelist (SSRF protection)
10
+ * - Download size limits (DoS protection)
11
+ * - Credential sanitization in errors
12
+ * - Retry with exponential backoff
13
+ * - Circuit breaker for gateway health tracking
14
+ *
15
+ * @module storage/ArweaveClient
16
+ *
17
+ * Key Principle: Arweave-First Write Order
18
+ * 1. Write to Arweave FIRST -> Get Arweave TX ID
19
+ * 2. THEN anchor TX ID on-chain
20
+ *
21
+ * @see AIP-7 §4.1 for invariant explanation
22
+ */
23
+ var __importDefault = (this && this.__importDefault) || function (mod) {
24
+ return (mod && mod.__esModule) ? mod : { "default": mod };
25
+ };
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.ArweaveClient = void 0;
28
+ const sdk_1 = __importDefault(require("@irys/sdk"));
29
+ const errors_1 = require("../errors");
30
+ const types_1 = require("./types");
31
+ const validation_1 = require("../utils/validation");
32
+ const retry_1 = require("../utils/retry");
33
+ const circuitBreaker_1 = require("../utils/circuitBreaker");
34
+ // ============================================================================
35
+ // Constants
36
+ // ============================================================================
37
+ const DEFAULT_CURRENCY = 'base-eth';
38
+ const DEFAULT_NETWORK = 'mainnet';
39
+ const DEFAULT_TIMEOUT = 60000; // 60 seconds
40
+ const DEFAULT_MAX_DOWNLOAD_SIZE = 10 * 1024 * 1024; // 10MB for archive bundles
41
+ const ARWEAVE_GATEWAY = 'https://arweave.net/';
42
+ // Minimum funding amount (to avoid dust)
43
+ const MIN_FUNDING_AMOUNT = 1000n; // 1000 wei minimum
44
+ // ============================================================================
45
+ // ArweaveClient Class
46
+ // ============================================================================
47
+ /**
48
+ * ArweaveClient - Permanent storage on Arweave via Irys
49
+ *
50
+ * Used for:
51
+ * - Archiving settled transaction bundles (compliance)
52
+ * - 7-year retention requirement (Arweave guarantees 200+ years)
53
+ *
54
+ * IMPORTANT: Uses Base ETH for payments (no bridging required)
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * const client = await ArweaveClient.create({
59
+ * privateKey: process.env.ARCHIVE_UPLOADER_KEY!,
60
+ * rpcUrl: process.env.BASE_RPC_URL!
61
+ * });
62
+ *
63
+ * // Check balance
64
+ * const balance = await client.getBalance();
65
+ * console.log('Irys balance:', balance);
66
+ *
67
+ * // Fund if needed
68
+ * if (balance < 10000n) {
69
+ * await client.fund(100000n);
70
+ * }
71
+ *
72
+ * // Upload archive bundle
73
+ * const result = await client.uploadBundle(archiveBundle);
74
+ * console.log('Arweave TX ID:', result.txId);
75
+ *
76
+ * // Download archive bundle
77
+ * const downloaded = await client.downloadBundle(result.txId);
78
+ * ```
79
+ */
80
+ class ArweaveClient {
81
+ /**
82
+ * Get initialized Irys instance (throws if not initialized)
83
+ */
84
+ get irys() {
85
+ if (!this.initialized || !this._irys) {
86
+ throw new errors_1.StorageError('operation', 'ArweaveClient not initialized. Call create() first.');
87
+ }
88
+ return this._irys;
89
+ }
90
+ /**
91
+ * Private constructor - use ArweaveClient.create() factory
92
+ */
93
+ constructor(config) {
94
+ this._irys = null;
95
+ this.initialized = false;
96
+ // Validate required config
97
+ if (!config.privateKey) {
98
+ throw new errors_1.ValidationError('privateKey', 'Private key is required for Arweave uploads');
99
+ }
100
+ if (!config.rpcUrl) {
101
+ throw new errors_1.ValidationError('rpcUrl', 'RPC URL is required for payment chain');
102
+ }
103
+ // P0-1: Validate default gateway URL against whitelist
104
+ (0, validation_1.validateGatewayURL)(ARWEAVE_GATEWAY, validation_1.ALLOWED_ARWEAVE_GATEWAYS, 'gatewayUrl');
105
+ this.config = {
106
+ privateKey: config.privateKey,
107
+ rpcUrl: config.rpcUrl,
108
+ currency: config.currency || DEFAULT_CURRENCY,
109
+ network: config.network || DEFAULT_NETWORK,
110
+ timeout: config.timeout || DEFAULT_TIMEOUT
111
+ };
112
+ // P1-1: DoS protection
113
+ this.maxDownloadSize = DEFAULT_MAX_DOWNLOAD_SIZE;
114
+ // P1-2: Default retry options
115
+ this.retryOptions = {
116
+ maxAttempts: 3,
117
+ initialDelayMs: 2000,
118
+ maxDelayMs: 30000,
119
+ backoffMultiplier: 2
120
+ };
121
+ // Circuit breaker for gateway health tracking
122
+ this.circuitBreakerEnabled = config.circuitBreaker?.enabled !== false;
123
+ if (this.circuitBreakerEnabled) {
124
+ this.circuitBreaker = new circuitBreaker_1.GatewayCircuitBreaker({
125
+ failureThreshold: config.circuitBreaker?.failureThreshold,
126
+ resetTimeoutMs: config.circuitBreaker?.resetTimeoutMs,
127
+ failureWindowMs: config.circuitBreaker?.failureWindowMs,
128
+ successThreshold: config.circuitBreaker?.successThreshold
129
+ });
130
+ }
131
+ else {
132
+ this.circuitBreaker = null;
133
+ }
134
+ }
135
+ /**
136
+ * Create and initialize ArweaveClient
137
+ *
138
+ * @param config - Arweave configuration
139
+ * @returns Initialized ArweaveClient
140
+ *
141
+ * @example
142
+ * ```typescript
143
+ * const client = await ArweaveClient.create({
144
+ * privateKey: process.env.ARCHIVE_UPLOADER_KEY!,
145
+ * rpcUrl: 'https://mainnet.base.org'
146
+ * });
147
+ * ```
148
+ */
149
+ static async create(config) {
150
+ const client = new ArweaveClient(config);
151
+ await client.initialize();
152
+ return client;
153
+ }
154
+ /**
155
+ * Initialize Irys SDK connection
156
+ */
157
+ async initialize() {
158
+ if (this.initialized)
159
+ return;
160
+ try {
161
+ this._irys = new sdk_1.default({
162
+ network: this.config.network,
163
+ token: this.config.currency,
164
+ key: this.config.privateKey,
165
+ config: {
166
+ providerUrl: this.config.rpcUrl
167
+ }
168
+ });
169
+ // Connect to Irys node
170
+ await this._irys.ready();
171
+ this.initialized = true;
172
+ }
173
+ catch (error) {
174
+ // P0-2: Sanitize error message (may contain credentials)
175
+ throw new errors_1.StorageError('initialization', `Failed to initialize Irys client: ${(0, validation_1.sanitizeErrorMessage)(error)}`, { currency: this.config.currency, network: this.config.network });
176
+ }
177
+ }
178
+ // ==========================================================================
179
+ // Funding Methods
180
+ // ==========================================================================
181
+ /**
182
+ * Fund the Irys node (required before uploading)
183
+ *
184
+ * @param amount - Amount to fund in payment currency (wei for ETH)
185
+ * @throws {ValidationError} If amount is invalid
186
+ * @throws {StorageError} If funding fails
187
+ *
188
+ * @example
189
+ * ```typescript
190
+ * // Fund with 0.001 ETH (1e15 wei)
191
+ * await client.fund(1000000000000000n);
192
+ * ```
193
+ */
194
+ async fund(amount) {
195
+ if (amount < MIN_FUNDING_AMOUNT) {
196
+ throw new errors_1.ValidationError('amount', `Funding amount too small. Minimum: ${MIN_FUNDING_AMOUNT} wei`);
197
+ }
198
+ try {
199
+ await this.withTimeout(this.irys.fund(amount), this.config.timeout, 'fund');
200
+ }
201
+ catch (error) {
202
+ if (error instanceof errors_1.ArweaveTimeoutError)
203
+ throw error;
204
+ // P0-2: Sanitize error message (may contain credentials)
205
+ throw new errors_1.StorageError('funding', `Failed to fund Irys: ${(0, validation_1.sanitizeErrorMessage)(error)}`, {
206
+ amount: amount.toString(),
207
+ currency: this.config.currency
208
+ });
209
+ }
210
+ }
211
+ /**
212
+ * Get current Irys balance
213
+ *
214
+ * @returns Balance in payment currency (wei for ETH)
215
+ *
216
+ * @example
217
+ * ```typescript
218
+ * const balance = await client.getBalance();
219
+ * console.log('Balance:', balance, 'wei');
220
+ * ```
221
+ */
222
+ async getBalance() {
223
+ try {
224
+ const balance = await this.irys.getLoadedBalance();
225
+ return BigInt(balance.toString());
226
+ }
227
+ catch (error) {
228
+ // P0-2: Sanitize error message
229
+ throw new errors_1.StorageError('balance', `Failed to get Irys balance: ${(0, validation_1.sanitizeErrorMessage)(error)}`);
230
+ }
231
+ }
232
+ /**
233
+ * Estimate cost of uploading data
234
+ *
235
+ * @param sizeBytes - Size of data to upload in bytes
236
+ * @returns Cost in payment currency (wei for ETH)
237
+ *
238
+ * @example
239
+ * ```typescript
240
+ * const cost = await client.estimateCost(50 * 1024); // 50KB
241
+ * console.log('Upload cost:', cost, 'wei');
242
+ * ```
243
+ */
244
+ async estimateCost(sizeBytes) {
245
+ if (sizeBytes <= 0) {
246
+ throw new errors_1.ValidationError('sizeBytes', 'Size must be positive');
247
+ }
248
+ try {
249
+ const price = await this.irys.getPrice(sizeBytes);
250
+ return BigInt(price.toString());
251
+ }
252
+ catch (error) {
253
+ // P0-2: Sanitize error message
254
+ throw new errors_1.StorageError('estimate', `Failed to estimate cost: ${(0, validation_1.sanitizeErrorMessage)(error)}`, {
255
+ sizeBytes
256
+ });
257
+ }
258
+ }
259
+ // ==========================================================================
260
+ // Upload Methods
261
+ // ==========================================================================
262
+ /**
263
+ * Upload archive bundle to Arweave (permanent storage)
264
+ *
265
+ * IMPORTANT: This is the first step in the archive flow.
266
+ * After getting the TX ID, anchor it on-chain via ArchiveTreasury.anchorArchive()
267
+ *
268
+ * @param bundle - Archive bundle to upload
269
+ * @returns Upload result with Arweave TX ID
270
+ * @throws {InsufficientBalanceError} If Irys balance is too low
271
+ * @throws {ArweaveUploadError} If upload fails
272
+ *
273
+ * @example
274
+ * ```typescript
275
+ * const result = await client.uploadBundle(bundle);
276
+ * console.log('Arweave TX ID:', result.txId);
277
+ *
278
+ * // Then anchor on-chain
279
+ * await archiveTreasury.anchorArchive(bundle.txId, result.txId);
280
+ * ```
281
+ */
282
+ async uploadBundle(bundle) {
283
+ // Validate bundle
284
+ this.validateBundle(bundle);
285
+ // Serialize bundle
286
+ const jsonString = JSON.stringify(bundle);
287
+ const buffer = Buffer.from(jsonString, 'utf-8');
288
+ // Check balance
289
+ const cost = await this.estimateCost(buffer.length);
290
+ const balance = await this.getBalance();
291
+ if (balance < cost) {
292
+ throw new errors_1.InsufficientBalanceError(cost.toString(), balance.toString(), this.config.currency.toUpperCase());
293
+ }
294
+ try {
295
+ // Upload with tags for discoverability
296
+ const tx = await this.withTimeout(this.irys.upload(buffer, {
297
+ tags: [
298
+ { name: 'Content-Type', value: 'application/json' },
299
+ { name: 'Protocol', value: 'AGIRAILS' },
300
+ { name: 'Version', value: bundle.protocolVersion },
301
+ { name: 'Schema', value: bundle.archiveSchemaVersion },
302
+ { name: 'Type', value: bundle.type },
303
+ { name: 'ChainId', value: bundle.chainId.toString() },
304
+ { name: 'TxId', value: bundle.txId }
305
+ ]
306
+ }), this.config.timeout, 'upload');
307
+ return {
308
+ txId: tx.id,
309
+ size: buffer.length,
310
+ uploadedAt: new Date(),
311
+ cost: cost.toString()
312
+ };
313
+ }
314
+ catch (error) {
315
+ if (error instanceof errors_1.ArweaveTimeoutError)
316
+ throw error;
317
+ if (error instanceof errors_1.InsufficientBalanceError)
318
+ throw error;
319
+ throw new errors_1.ArweaveUploadError(error.message || 'Upload failed', {
320
+ bundleTxId: bundle.txId,
321
+ size: buffer.length
322
+ });
323
+ }
324
+ }
325
+ /**
326
+ * Upload arbitrary JSON to Arweave
327
+ *
328
+ * For general-purpose permanent storage (not archive bundles).
329
+ *
330
+ * @param data - JSON data to upload
331
+ * @param tags - Optional tags for discoverability
332
+ * @returns Upload result with Arweave TX ID
333
+ */
334
+ async uploadJSON(data, tags) {
335
+ const jsonString = JSON.stringify(data);
336
+ const buffer = Buffer.from(jsonString, 'utf-8');
337
+ // Check balance
338
+ const cost = await this.estimateCost(buffer.length);
339
+ const balance = await this.getBalance();
340
+ if (balance < cost) {
341
+ throw new errors_1.InsufficientBalanceError(cost.toString(), balance.toString(), this.config.currency.toUpperCase());
342
+ }
343
+ try {
344
+ const defaultTags = [
345
+ { name: 'Content-Type', value: 'application/json' },
346
+ { name: 'Protocol', value: 'AGIRAILS' }
347
+ ];
348
+ const tx = await this.withTimeout(this.irys.upload(buffer, {
349
+ tags: tags ? [...defaultTags, ...tags] : defaultTags
350
+ }), this.config.timeout, 'upload');
351
+ return {
352
+ txId: tx.id,
353
+ size: buffer.length,
354
+ uploadedAt: new Date(),
355
+ cost: cost.toString()
356
+ };
357
+ }
358
+ catch (error) {
359
+ if (error instanceof errors_1.ArweaveTimeoutError)
360
+ throw error;
361
+ if (error instanceof errors_1.InsufficientBalanceError)
362
+ throw error;
363
+ throw new errors_1.ArweaveUploadError(error.message || 'JSON upload failed', {
364
+ size: buffer.length
365
+ });
366
+ }
367
+ }
368
+ // ==========================================================================
369
+ // Download Methods
370
+ // ==========================================================================
371
+ /**
372
+ * Download archive bundle from Arweave
373
+ *
374
+ * Security Features:
375
+ * - Size limit enforcement (P1-1: DoS protection)
376
+ * - Retry with exponential backoff (P1-2)
377
+ * - Centralized TX ID validation (P1-3)
378
+ * - Credential sanitization in errors (P0-2)
379
+ *
380
+ * @param txId - Arweave transaction ID
381
+ * @returns Downloaded archive bundle
382
+ * @throws {InvalidArweaveTxIdError} If TX ID format is invalid
383
+ * @throws {FileSizeLimitExceededError} If content exceeds size limit
384
+ * @throws {ArweaveDownloadError} If download fails
385
+ *
386
+ * @example
387
+ * ```typescript
388
+ * const result = await client.downloadBundle('h7Xk2...');
389
+ * console.log('Bundle:', result.data);
390
+ * ```
391
+ */
392
+ async downloadBundle(txId) {
393
+ // P1-3: Use centralized TX ID validation
394
+ (0, validation_1.validateArweaveTxId)(txId);
395
+ const url = `${ARWEAVE_GATEWAY}${txId}`;
396
+ // Circuit breaker: check gateway health before attempting download
397
+ if (this.circuitBreaker && !this.circuitBreaker.isHealthy(ARWEAVE_GATEWAY)) {
398
+ const state = this.circuitBreaker.getState(ARWEAVE_GATEWAY);
399
+ const failures = this.circuitBreaker.getFailureCount(ARWEAVE_GATEWAY);
400
+ throw new errors_1.ArweaveDownloadError(txId, `Gateway circuit breaker OPEN for ${ARWEAVE_GATEWAY}. ` +
401
+ `State: ${state}, Failures: ${failures}. ` +
402
+ `Please wait for cooldown period before retrying.`);
403
+ }
404
+ // P1-2: Wrap in retry logic
405
+ return (0, retry_1.withRetry)(async () => {
406
+ const controller = new AbortController();
407
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
408
+ try {
409
+ const response = await fetch(url, {
410
+ signal: controller.signal,
411
+ headers: { 'Accept': 'application/json' }
412
+ });
413
+ clearTimeout(timeoutId);
414
+ if (!response.ok) {
415
+ throw new errors_1.ArweaveDownloadError(txId, `HTTP ${response.status}: ${response.statusText}`);
416
+ }
417
+ // P1-1: Check Content-Length header before downloading
418
+ const contentLength = response.headers.get('content-length');
419
+ if (contentLength && parseInt(contentLength, 10) > this.maxDownloadSize) {
420
+ throw new errors_1.FileSizeLimitExceededError(parseInt(contentLength, 10), this.maxDownloadSize);
421
+ }
422
+ // P1-1: Stream response with size limit enforcement
423
+ const reader = response.body?.getReader();
424
+ if (!reader) {
425
+ throw new errors_1.ArweaveDownloadError(txId, 'No response body');
426
+ }
427
+ const chunks = [];
428
+ let totalSize = 0;
429
+ while (true) {
430
+ const { done, value } = await reader.read();
431
+ if (done)
432
+ break;
433
+ totalSize += value.length;
434
+ // P1-1: Enforce size limit during streaming
435
+ if (totalSize > this.maxDownloadSize) {
436
+ reader.cancel();
437
+ throw new errors_1.FileSizeLimitExceededError(totalSize, this.maxDownloadSize);
438
+ }
439
+ chunks.push(value);
440
+ }
441
+ // Combine chunks into text
442
+ const decoder = new TextDecoder();
443
+ const text = chunks.map(chunk => decoder.decode(chunk, { stream: true })).join('') +
444
+ decoder.decode();
445
+ const data = JSON.parse(text);
446
+ // Validate it's actually an archive bundle
447
+ if (data.type !== types_1.ARCHIVE_BUNDLE_TYPE) {
448
+ throw new errors_1.ArweaveDownloadError(txId, `Invalid bundle type: ${data.type}. Expected: ${types_1.ARCHIVE_BUNDLE_TYPE}`);
449
+ }
450
+ // Circuit breaker: record success
451
+ this.circuitBreaker?.recordSuccess(ARWEAVE_GATEWAY);
452
+ return {
453
+ data,
454
+ size: totalSize,
455
+ downloadedAt: new Date()
456
+ };
457
+ }
458
+ catch (error) {
459
+ clearTimeout(timeoutId);
460
+ // Circuit breaker: record failure only for gateway issues (not content errors)
461
+ if (this.isGatewayFailure(error)) {
462
+ this.circuitBreaker?.recordFailure(ARWEAVE_GATEWAY);
463
+ }
464
+ if (error instanceof errors_1.ArweaveDownloadError)
465
+ throw error;
466
+ if (error instanceof errors_1.FileSizeLimitExceededError)
467
+ throw error;
468
+ if (error.name === 'AbortError') {
469
+ throw new errors_1.ArweaveTimeoutError('download', this.config.timeout);
470
+ }
471
+ if (error instanceof SyntaxError) {
472
+ throw new errors_1.ArweaveDownloadError(txId, 'Invalid JSON content');
473
+ }
474
+ // P0-2: Sanitize error message
475
+ throw new errors_1.ArweaveDownloadError(txId, (0, validation_1.sanitizeErrorMessage)(error));
476
+ }
477
+ }, this.retryOptions);
478
+ }
479
+ /**
480
+ * Download arbitrary JSON from Arweave
481
+ *
482
+ * Security Features:
483
+ * - Size limit enforcement (P1-1: DoS protection)
484
+ * - Retry with exponential backoff (P1-2)
485
+ * - Centralized TX ID validation (P1-3)
486
+ *
487
+ * @param txId - Arweave transaction ID
488
+ * @returns Downloaded JSON data
489
+ */
490
+ async downloadJSON(txId) {
491
+ // P1-3: Use centralized TX ID validation
492
+ (0, validation_1.validateArweaveTxId)(txId);
493
+ const url = `${ARWEAVE_GATEWAY}${txId}`;
494
+ // Circuit breaker: check gateway health before attempting download
495
+ if (this.circuitBreaker && !this.circuitBreaker.isHealthy(ARWEAVE_GATEWAY)) {
496
+ const state = this.circuitBreaker.getState(ARWEAVE_GATEWAY);
497
+ const failures = this.circuitBreaker.getFailureCount(ARWEAVE_GATEWAY);
498
+ throw new errors_1.ArweaveDownloadError(txId, `Gateway circuit breaker OPEN for ${ARWEAVE_GATEWAY}. ` +
499
+ `State: ${state}, Failures: ${failures}. ` +
500
+ `Please wait for cooldown period before retrying.`);
501
+ }
502
+ // P1-2: Wrap in retry logic
503
+ return (0, retry_1.withRetry)(async () => {
504
+ const controller = new AbortController();
505
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
506
+ try {
507
+ const response = await fetch(url, {
508
+ signal: controller.signal,
509
+ headers: { 'Accept': 'application/json' }
510
+ });
511
+ clearTimeout(timeoutId);
512
+ if (!response.ok) {
513
+ throw new errors_1.ArweaveDownloadError(txId, `HTTP ${response.status}: ${response.statusText}`);
514
+ }
515
+ // P1-1: Check Content-Length header before downloading
516
+ const contentLength = response.headers.get('content-length');
517
+ if (contentLength && parseInt(contentLength, 10) > this.maxDownloadSize) {
518
+ throw new errors_1.FileSizeLimitExceededError(parseInt(contentLength, 10), this.maxDownloadSize);
519
+ }
520
+ // P1-1: Stream response with size limit enforcement
521
+ const reader = response.body?.getReader();
522
+ if (!reader) {
523
+ throw new errors_1.ArweaveDownloadError(txId, 'No response body');
524
+ }
525
+ const chunks = [];
526
+ let totalSize = 0;
527
+ while (true) {
528
+ const { done, value } = await reader.read();
529
+ if (done)
530
+ break;
531
+ totalSize += value.length;
532
+ // P1-1: Enforce size limit during streaming
533
+ if (totalSize > this.maxDownloadSize) {
534
+ reader.cancel();
535
+ throw new errors_1.FileSizeLimitExceededError(totalSize, this.maxDownloadSize);
536
+ }
537
+ chunks.push(value);
538
+ }
539
+ // Combine chunks into text
540
+ const decoder = new TextDecoder();
541
+ const text = chunks.map(chunk => decoder.decode(chunk, { stream: true })).join('') +
542
+ decoder.decode();
543
+ const data = JSON.parse(text);
544
+ // Circuit breaker: record success
545
+ this.circuitBreaker?.recordSuccess(ARWEAVE_GATEWAY);
546
+ return {
547
+ data,
548
+ size: totalSize,
549
+ downloadedAt: new Date()
550
+ };
551
+ }
552
+ catch (error) {
553
+ clearTimeout(timeoutId);
554
+ // Circuit breaker: record failure only for gateway issues (not content errors)
555
+ if (this.isGatewayFailure(error)) {
556
+ this.circuitBreaker?.recordFailure(ARWEAVE_GATEWAY);
557
+ }
558
+ if (error instanceof errors_1.ArweaveDownloadError)
559
+ throw error;
560
+ if (error instanceof errors_1.FileSizeLimitExceededError)
561
+ throw error;
562
+ if (error.name === 'AbortError') {
563
+ throw new errors_1.ArweaveTimeoutError('download', this.config.timeout);
564
+ }
565
+ if (error instanceof SyntaxError) {
566
+ throw new errors_1.ArweaveDownloadError(txId, 'Invalid JSON content');
567
+ }
568
+ // P0-2: Sanitize error message
569
+ throw new errors_1.ArweaveDownloadError(txId, (0, validation_1.sanitizeErrorMessage)(error));
570
+ }
571
+ }, this.retryOptions);
572
+ }
573
+ /**
574
+ * Check if content exists on Arweave
575
+ *
576
+ * @param txId - Arweave transaction ID
577
+ * @returns True if content exists
578
+ */
579
+ async exists(txId) {
580
+ // P1-3: Use centralized TX ID validation
581
+ (0, validation_1.validateArweaveTxId)(txId);
582
+ const url = `${ARWEAVE_GATEWAY}${txId}`;
583
+ try {
584
+ const controller = new AbortController();
585
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
586
+ const response = await fetch(url, {
587
+ method: 'HEAD',
588
+ signal: controller.signal
589
+ });
590
+ clearTimeout(timeoutId);
591
+ return response.ok;
592
+ }
593
+ catch {
594
+ return false;
595
+ }
596
+ }
597
+ // ==========================================================================
598
+ // Getters
599
+ // ==========================================================================
600
+ /**
601
+ * Get configured currency
602
+ */
603
+ getCurrency() {
604
+ return this.config.currency;
605
+ }
606
+ /**
607
+ * Get configured network
608
+ */
609
+ getNetwork() {
610
+ return this.config.network;
611
+ }
612
+ /**
613
+ * Get wallet address
614
+ */
615
+ async getAddress() {
616
+ return this.irys.address;
617
+ }
618
+ // ==========================================================================
619
+ // Private Methods
620
+ // ==========================================================================
621
+ /**
622
+ * Validate archive bundle structure
623
+ */
624
+ validateBundle(bundle) {
625
+ if (!bundle) {
626
+ throw new errors_1.ValidationError('bundle', 'Archive bundle is required');
627
+ }
628
+ if (bundle.type !== types_1.ARCHIVE_BUNDLE_TYPE) {
629
+ throw new errors_1.ValidationError('bundle.type', `Invalid bundle type: ${bundle.type}. Expected: ${types_1.ARCHIVE_BUNDLE_TYPE}`);
630
+ }
631
+ if (!bundle.txId || !/^0x[a-fA-F0-9]{64}$/.test(bundle.txId)) {
632
+ throw new errors_1.ValidationError('bundle.txId', 'Invalid transaction ID format');
633
+ }
634
+ if (![8453, 84532].includes(bundle.chainId)) {
635
+ throw new errors_1.ValidationError('bundle.chainId', `Invalid chain ID: ${bundle.chainId}. Expected 8453 or 84532`);
636
+ }
637
+ if (!bundle.participants?.requester || !bundle.participants?.provider) {
638
+ throw new errors_1.ValidationError('bundle.participants', 'Both requester and provider required');
639
+ }
640
+ if (!bundle.references?.requestCID || !bundle.references?.deliveryCID) {
641
+ throw new errors_1.ValidationError('bundle.references', 'Both requestCID and deliveryCID required');
642
+ }
643
+ if (!bundle.hashes?.requestHash || !bundle.hashes?.deliveryHash || !bundle.hashes?.serviceHash) {
644
+ throw new errors_1.ValidationError('bundle.hashes', 'All hashes required');
645
+ }
646
+ if (!bundle.settlement) {
647
+ throw new errors_1.ValidationError('bundle.settlement', 'Settlement info required');
648
+ }
649
+ }
650
+ /**
651
+ * Wrap promise with timeout
652
+ */
653
+ async withTimeout(promise, timeoutMs, operation) {
654
+ let timeoutId;
655
+ const timeoutPromise = new Promise((_, reject) => {
656
+ timeoutId = setTimeout(() => {
657
+ reject(new errors_1.ArweaveTimeoutError(operation, timeoutMs));
658
+ }, timeoutMs);
659
+ });
660
+ try {
661
+ return await Promise.race([promise, timeoutPromise]);
662
+ }
663
+ finally {
664
+ clearTimeout(timeoutId);
665
+ }
666
+ }
667
+ /**
668
+ * Determine if an error represents a gateway failure vs content-specific error
669
+ *
670
+ * Gateway failures (should trigger circuit breaker):
671
+ * - 5xx HTTP errors (server errors)
672
+ * - Network timeouts
673
+ * - Connection refused
674
+ * - DNS resolution failures
675
+ *
676
+ * Content-specific errors (should NOT trigger circuit breaker):
677
+ * - 404 (content not found - not gateway's fault)
678
+ * - 400 (bad request - client's fault)
679
+ * - SyntaxError (invalid JSON - content issue)
680
+ * - Invalid bundle type (content validation failed)
681
+ */
682
+ isGatewayFailure(error) {
683
+ // Network-level errors are always gateway failures
684
+ if (error.name === 'AbortError')
685
+ return true; // Timeout
686
+ if (error.code === 'ECONNREFUSED')
687
+ return true;
688
+ if (error.code === 'ENOTFOUND')
689
+ return true;
690
+ if (error.code === 'ETIMEDOUT')
691
+ return true;
692
+ // ArweaveTimeoutError is a gateway failure
693
+ if (error instanceof errors_1.ArweaveTimeoutError)
694
+ return true;
695
+ // Content-specific errors are NOT gateway failures
696
+ if (error instanceof SyntaxError)
697
+ return false; // Invalid JSON
698
+ if (error instanceof errors_1.FileSizeLimitExceededError)
699
+ return false;
700
+ // Check for HTTP status codes in ArweaveDownloadError
701
+ if (error instanceof errors_1.ArweaveDownloadError) {
702
+ const message = error.message || '';
703
+ // 5xx errors are gateway failures
704
+ if (/HTTP 5\d{2}/.test(message))
705
+ return true;
706
+ // 4xx errors are NOT gateway failures (content/client issues)
707
+ if (/HTTP 4\d{2}/.test(message))
708
+ return false;
709
+ // "Invalid bundle type" is content validation failure
710
+ if (message.includes('Invalid bundle type'))
711
+ return false;
712
+ // "Invalid JSON content" is content error
713
+ if (message.includes('Invalid JSON'))
714
+ return false;
715
+ // Default: treat unknown ArweaveDownloadError as potential gateway issue
716
+ return true;
717
+ }
718
+ // Generic errors (TypeError, etc.) could be network issues
719
+ return true;
720
+ }
721
+ // ==========================================================================
722
+ // Circuit Breaker Methods
723
+ // ==========================================================================
724
+ /**
725
+ * Get circuit breaker status for Arweave gateway
726
+ *
727
+ * @returns Circuit breaker status or null if disabled
728
+ *
729
+ * @example
730
+ * ```typescript
731
+ * const status = client.getCircuitBreakerStatus();
732
+ * if (status && status.state === 'OPEN') {
733
+ * console.log('Gateway unhealthy, failures:', status.failures);
734
+ * }
735
+ * ```
736
+ */
737
+ getCircuitBreakerStatus() {
738
+ if (!this.circuitBreaker)
739
+ return null;
740
+ return {
741
+ state: this.circuitBreaker.getState(ARWEAVE_GATEWAY),
742
+ failures: this.circuitBreaker.getFailureCount(ARWEAVE_GATEWAY)
743
+ };
744
+ }
745
+ /**
746
+ * Reset circuit breaker for Arweave gateway
747
+ *
748
+ * Use with caution - only reset when you're confident the gateway is healthy.
749
+ * Useful for testing or after manual verification.
750
+ *
751
+ * @example
752
+ * ```typescript
753
+ * client.resetCircuitBreaker();
754
+ * ```
755
+ */
756
+ resetCircuitBreaker() {
757
+ this.circuitBreaker?.reset(ARWEAVE_GATEWAY);
758
+ }
759
+ }
760
+ exports.ArweaveClient = ArweaveClient;
761
+ //# sourceMappingURL=ArweaveClient.js.map