@agirails/sdk 2.0.4 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +536 -87
- package/dist/ACTPClient.d.ts +200 -0
- package/dist/ACTPClient.d.ts.map +1 -1
- package/dist/ACTPClient.js +266 -2
- package/dist/ACTPClient.js.map +1 -1
- package/dist/abi/ACTPKernel.json +16 -0
- package/dist/adapters/AdapterRegistry.d.ts +140 -0
- package/dist/adapters/AdapterRegistry.d.ts.map +1 -0
- package/dist/adapters/AdapterRegistry.js +166 -0
- package/dist/adapters/AdapterRegistry.js.map +1 -0
- package/dist/adapters/AdapterRouter.d.ts +165 -0
- package/dist/adapters/AdapterRouter.d.ts.map +1 -0
- package/dist/adapters/AdapterRouter.js +350 -0
- package/dist/adapters/AdapterRouter.js.map +1 -0
- package/dist/adapters/BaseAdapter.d.ts +17 -0
- package/dist/adapters/BaseAdapter.d.ts.map +1 -1
- package/dist/adapters/BaseAdapter.js +21 -0
- package/dist/adapters/BaseAdapter.js.map +1 -1
- package/dist/adapters/BasicAdapter.d.ts +72 -3
- package/dist/adapters/BasicAdapter.d.ts.map +1 -1
- package/dist/adapters/BasicAdapter.js +178 -2
- package/dist/adapters/BasicAdapter.js.map +1 -1
- package/dist/adapters/IAdapter.d.ts +230 -0
- package/dist/adapters/IAdapter.d.ts.map +1 -0
- package/dist/adapters/IAdapter.js +44 -0
- package/dist/adapters/IAdapter.js.map +1 -0
- package/dist/adapters/StandardAdapter.d.ts +80 -6
- package/dist/adapters/StandardAdapter.d.ts.map +1 -1
- package/dist/adapters/StandardAdapter.js +203 -6
- package/dist/adapters/StandardAdapter.js.map +1 -1
- package/dist/adapters/X402Adapter.d.ts +208 -0
- package/dist/adapters/X402Adapter.d.ts.map +1 -0
- package/dist/adapters/X402Adapter.js +423 -0
- package/dist/adapters/X402Adapter.js.map +1 -0
- package/dist/adapters/index.d.ts +8 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +19 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/cli/commands/init.d.ts +4 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +146 -4
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/config/networks.d.ts +9 -0
- package/dist/config/networks.d.ts.map +1 -1
- package/dist/config/networks.js +27 -12
- package/dist/config/networks.js.map +1 -1
- package/dist/erc8004/ERC8004Bridge.d.ts +155 -0
- package/dist/erc8004/ERC8004Bridge.d.ts.map +1 -0
- package/dist/erc8004/ERC8004Bridge.js +325 -0
- package/dist/erc8004/ERC8004Bridge.js.map +1 -0
- package/dist/erc8004/ReputationReporter.d.ts +223 -0
- package/dist/erc8004/ReputationReporter.d.ts.map +1 -0
- package/dist/erc8004/ReputationReporter.js +266 -0
- package/dist/erc8004/ReputationReporter.js.map +1 -0
- package/dist/erc8004/index.d.ts +36 -0
- package/dist/erc8004/index.d.ts.map +1 -0
- package/dist/erc8004/index.js +46 -0
- package/dist/erc8004/index.js.map +1 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +51 -2
- package/dist/index.js.map +1 -1
- package/dist/level0/provide.d.ts.map +1 -1
- package/dist/level0/provide.js +2 -1
- package/dist/level0/provide.js.map +1 -1
- package/dist/level1/Agent.d.ts.map +1 -1
- package/dist/level1/Agent.js +11 -3
- package/dist/level1/Agent.js.map +1 -1
- package/dist/protocol/ACTPKernel.d.ts +1 -1
- package/dist/protocol/ACTPKernel.d.ts.map +1 -1
- package/dist/protocol/ACTPKernel.js +23 -12
- package/dist/protocol/ACTPKernel.js.map +1 -1
- package/dist/protocol/DIDResolver.js +1 -1
- package/dist/protocol/DIDResolver.js.map +1 -1
- package/dist/protocol/EASHelper.d.ts.map +1 -1
- package/dist/protocol/EASHelper.js +2 -3
- package/dist/protocol/EASHelper.js.map +1 -1
- package/dist/protocol/MessageSigner.d.ts.map +1 -1
- package/dist/protocol/MessageSigner.js +8 -8
- package/dist/protocol/MessageSigner.js.map +1 -1
- package/dist/runtime/BlockchainRuntime.d.ts +7 -0
- package/dist/runtime/BlockchainRuntime.d.ts.map +1 -1
- package/dist/runtime/BlockchainRuntime.js +40 -22
- package/dist/runtime/BlockchainRuntime.js.map +1 -1
- package/dist/runtime/IACTPRuntime.d.ts +21 -0
- package/dist/runtime/IACTPRuntime.d.ts.map +1 -1
- package/dist/runtime/MockRuntime.d.ts +19 -0
- package/dist/runtime/MockRuntime.d.ts.map +1 -1
- package/dist/runtime/MockRuntime.js +56 -4
- package/dist/runtime/MockRuntime.js.map +1 -1
- package/dist/runtime/types/MockState.d.ts +11 -2
- package/dist/runtime/types/MockState.d.ts.map +1 -1
- package/dist/runtime/types/MockState.js.map +1 -1
- package/dist/storage/ArchiveBundleBuilder.d.ts +150 -0
- package/dist/storage/ArchiveBundleBuilder.d.ts.map +1 -0
- package/dist/storage/ArchiveBundleBuilder.js +468 -0
- package/dist/storage/ArchiveBundleBuilder.js.map +1 -0
- package/dist/storage/ArweaveClient.d.ts +271 -0
- package/dist/storage/ArweaveClient.d.ts.map +1 -0
- package/dist/storage/ArweaveClient.js +761 -0
- package/dist/storage/ArweaveClient.js.map +1 -0
- package/dist/storage/FilebaseClient.d.ts +193 -0
- package/dist/storage/FilebaseClient.d.ts.map +1 -0
- package/dist/storage/FilebaseClient.js +643 -0
- package/dist/storage/FilebaseClient.js.map +1 -0
- package/dist/storage/index.d.ts +47 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +64 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/types.d.ts +291 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/storage/types.js +18 -0
- package/dist/storage/types.js.map +1 -0
- package/dist/types/adapter.d.ts +359 -0
- package/dist/types/adapter.d.ts.map +1 -0
- package/dist/types/adapter.js +115 -0
- package/dist/types/adapter.js.map +1 -0
- package/dist/types/erc8004.d.ts +184 -0
- package/dist/types/erc8004.d.ts.map +1 -0
- package/dist/types/erc8004.js +132 -0
- package/dist/types/erc8004.js.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/state.d.ts +5 -4
- package/dist/types/state.d.ts.map +1 -1
- package/dist/types/state.js +10 -9
- package/dist/types/state.js.map +1 -1
- package/dist/types/transaction.d.ts +12 -0
- package/dist/types/transaction.d.ts.map +1 -1
- package/dist/types/x402.d.ts +162 -0
- package/dist/types/x402.d.ts.map +1 -0
- package/dist/types/x402.js +162 -0
- package/dist/types/x402.js.map +1 -0
- package/dist/utils/IPFSClient.d.ts.map +1 -1
- package/dist/utils/IPFSClient.js +5 -2
- package/dist/utils/IPFSClient.js.map +1 -1
- package/dist/utils/NonceManager.d.ts.map +1 -1
- package/dist/utils/NonceManager.js +3 -2
- package/dist/utils/NonceManager.js.map +1 -1
- package/dist/utils/UsedAttestationTracker.d.ts.map +1 -1
- package/dist/utils/UsedAttestationTracker.js +3 -3
- package/dist/utils/UsedAttestationTracker.js.map +1 -1
- package/dist/utils/circuitBreaker.d.ts +136 -0
- package/dist/utils/circuitBreaker.d.ts.map +1 -0
- package/dist/utils/circuitBreaker.js +253 -0
- package/dist/utils/circuitBreaker.js.map +1 -0
- package/dist/utils/retry.d.ts +120 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +260 -0
- package/dist/utils/retry.js.map +1 -0
- package/dist/utils/validation.d.ts +100 -0
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +248 -1
- package/dist/utils/validation.js.map +1 -1
- package/package.json +16 -3
- package/src/ACTPClient.ts +318 -2
- package/src/abi/ACTPKernel.json +16 -0
- package/src/adapters/AdapterRegistry.ts +173 -0
- package/src/adapters/AdapterRouter.ts +417 -0
- package/src/adapters/BaseAdapter.ts +25 -0
- package/src/adapters/BasicAdapter.ts +210 -3
- package/src/adapters/IAdapter.ts +292 -0
- package/src/adapters/StandardAdapter.ts +246 -7
- package/src/adapters/X402Adapter.ts +653 -0
- package/src/adapters/index.ts +27 -0
- package/src/cli/commands/init.ts +166 -3
- package/src/config/networks.ts +36 -12
- package/src/erc8004/ERC8004Bridge.ts +461 -0
- package/src/erc8004/ReputationReporter.ts +472 -0
- package/src/erc8004/index.ts +61 -0
- package/src/index.ts +97 -0
- package/src/level0/provide.ts +2 -1
- package/src/level1/Agent.ts +13 -3
- package/src/protocol/ACTPKernel.ts +33 -12
- package/src/protocol/DIDResolver.ts +1 -1
- package/src/protocol/EASHelper.ts +2 -5
- package/src/protocol/MessageSigner.ts +8 -14
- package/src/runtime/BlockchainRuntime.ts +41 -45
- package/src/runtime/IACTPRuntime.ts +22 -0
- package/src/runtime/MockRuntime.ts +58 -4
- package/src/runtime/types/MockState.ts +12 -2
- package/src/storage/ArchiveBundleBuilder.ts +563 -0
- package/src/storage/ArweaveClient.ts +945 -0
- package/src/storage/FilebaseClient.ts +790 -0
- package/src/storage/index.ts +96 -0
- package/src/storage/types.ts +348 -0
- package/src/types/adapter.ts +296 -0
- package/src/types/erc8004.ts +293 -0
- package/src/types/index.ts +3 -0
- package/src/types/state.ts +10 -9
- package/src/types/transaction.ts +12 -0
- package/src/types/x402.ts +219 -0
- package/src/utils/IPFSClient.ts +5 -4
- package/src/utils/NonceManager.ts +3 -2
- package/src/utils/UsedAttestationTracker.ts +3 -5
- package/src/utils/circuitBreaker.ts +324 -0
- package/src/utils/fsSafe.ts +5 -0
- package/src/utils/retry.ts +365 -0
- package/src/utils/validation.ts +295 -1
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* X402 Protocol Types and Constants
|
|
3
|
+
*
|
|
4
|
+
* Defines types for the HTTP 402 Payment Required protocol implementation.
|
|
5
|
+
* X402 enables atomic, instant API payments - NO escrow, NO state machine.
|
|
6
|
+
*
|
|
7
|
+
* Key difference from ACTP:
|
|
8
|
+
* - ACTP: escrow → state machine → disputes → explicit release
|
|
9
|
+
* - x402: atomic payment → instant settlement → done
|
|
10
|
+
*
|
|
11
|
+
* Flow:
|
|
12
|
+
* 1. Client requests protected HTTPS endpoint → gets 402 response
|
|
13
|
+
* 2. Parse X-Payment-* headers (address, amount, network, deadline)
|
|
14
|
+
* 3. Execute atomic USDC transfer to provider (no escrow!)
|
|
15
|
+
* 4. Retry request with tx hash as proof
|
|
16
|
+
* 5. Return response - payment complete, no release needed
|
|
17
|
+
*
|
|
18
|
+
* Use x402 for: Simple API calls, instant delivery, low-value transactions
|
|
19
|
+
* Use ACTP for: Complex services, dispute protection, high-value transactions
|
|
20
|
+
*
|
|
21
|
+
* @module types/x402
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// X402 Header Constants
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Standard X402 payment headers sent in 402 responses.
|
|
30
|
+
*
|
|
31
|
+
* These headers inform the client how to pay for the requested resource.
|
|
32
|
+
*/
|
|
33
|
+
export const X402_HEADERS = {
|
|
34
|
+
/** Indicates payment is required (must be "true") */
|
|
35
|
+
REQUIRED: 'x-payment-required',
|
|
36
|
+
/** Provider's wallet address (0x-prefixed) */
|
|
37
|
+
ADDRESS: 'x-payment-address',
|
|
38
|
+
/** Amount in USDC wei (6 decimals) */
|
|
39
|
+
AMOUNT: 'x-payment-amount',
|
|
40
|
+
/** Network identifier (base-mainnet or base-sepolia) */
|
|
41
|
+
NETWORK: 'x-payment-network',
|
|
42
|
+
/** Token type (currently only USDC) */
|
|
43
|
+
TOKEN: 'x-payment-token',
|
|
44
|
+
/** Unix timestamp deadline for payment */
|
|
45
|
+
DEADLINE: 'x-payment-deadline',
|
|
46
|
+
/** Optional: Dispute window in seconds */
|
|
47
|
+
DISPUTE_WINDOW: 'x-dispute-window',
|
|
48
|
+
/** Optional: Service identifier for tracking */
|
|
49
|
+
SERVICE_ID: 'x-service-id',
|
|
50
|
+
} as const;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Proof headers sent when retrying with payment.
|
|
54
|
+
*
|
|
55
|
+
* After creating an ACTP transaction, the client includes these
|
|
56
|
+
* headers to prove payment was made.
|
|
57
|
+
*/
|
|
58
|
+
export const X402_PROOF_HEADERS = {
|
|
59
|
+
/** ACTP transaction ID (bytes32 hex) */
|
|
60
|
+
TX_ID: 'x-payment-tx-id',
|
|
61
|
+
/** ACTP escrow ID (bytes32 hex) */
|
|
62
|
+
ESCROW_ID: 'x-payment-escrow-id',
|
|
63
|
+
} as const;
|
|
64
|
+
|
|
65
|
+
// ============================================================================
|
|
66
|
+
// X402 Types
|
|
67
|
+
// ============================================================================
|
|
68
|
+
|
|
69
|
+
/** Supported networks for x402 payments */
|
|
70
|
+
export type X402Network = 'base-mainnet' | 'base-sepolia';
|
|
71
|
+
|
|
72
|
+
/** Supported tokens (currently only USDC) */
|
|
73
|
+
export type X402Token = 'USDC';
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Parsed payment headers from HTTP 402 response.
|
|
77
|
+
*
|
|
78
|
+
* Contains all information needed to create an ACTP transaction
|
|
79
|
+
* for the requested resource.
|
|
80
|
+
*/
|
|
81
|
+
export interface X402PaymentHeaders {
|
|
82
|
+
/** Whether payment is required (always true for valid 402) */
|
|
83
|
+
required: boolean;
|
|
84
|
+
|
|
85
|
+
/** Provider's wallet address (0x-prefixed, 42 chars) */
|
|
86
|
+
paymentAddress: string;
|
|
87
|
+
|
|
88
|
+
/** Amount in USDC wei (6 decimals, as string) */
|
|
89
|
+
amount: string;
|
|
90
|
+
|
|
91
|
+
/** Target network */
|
|
92
|
+
network: X402Network;
|
|
93
|
+
|
|
94
|
+
/** Token type */
|
|
95
|
+
token: X402Token;
|
|
96
|
+
|
|
97
|
+
/** Unix timestamp deadline for accepting payment */
|
|
98
|
+
deadline: number;
|
|
99
|
+
|
|
100
|
+
/** Optional: Dispute window in seconds */
|
|
101
|
+
disputeWindow?: number;
|
|
102
|
+
|
|
103
|
+
/** Optional: Service identifier for tracking/debugging */
|
|
104
|
+
serviceId?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ============================================================================
|
|
108
|
+
// X402 Error Handling
|
|
109
|
+
// ============================================================================
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Error codes for X402 protocol failures.
|
|
113
|
+
*
|
|
114
|
+
* Used to identify specific error types and enable proper error handling.
|
|
115
|
+
*/
|
|
116
|
+
export enum X402ErrorCode {
|
|
117
|
+
/** Response status was not 402 */
|
|
118
|
+
NOT_402_RESPONSE = 'NOT_402_RESPONSE',
|
|
119
|
+
|
|
120
|
+
/** Required X-Payment-* headers are missing */
|
|
121
|
+
MISSING_HEADERS = 'MISSING_HEADERS',
|
|
122
|
+
|
|
123
|
+
/** X-Payment-Amount is invalid (not a number, negative, etc.) */
|
|
124
|
+
INVALID_AMOUNT = 'INVALID_AMOUNT',
|
|
125
|
+
|
|
126
|
+
/** X-Payment-Address is not a valid Ethereum address */
|
|
127
|
+
INVALID_ADDRESS = 'INVALID_ADDRESS',
|
|
128
|
+
|
|
129
|
+
/** X-Payment-Network is not a recognized network */
|
|
130
|
+
INVALID_NETWORK = 'INVALID_NETWORK',
|
|
131
|
+
|
|
132
|
+
/** Server's network doesn't match client's expected network */
|
|
133
|
+
NETWORK_MISMATCH = 'NETWORK_MISMATCH',
|
|
134
|
+
|
|
135
|
+
/** Failed to create ACTP transaction or link escrow */
|
|
136
|
+
PAYMENT_FAILED = 'PAYMENT_FAILED',
|
|
137
|
+
|
|
138
|
+
/** Retry request with proof headers failed */
|
|
139
|
+
RETRY_FAILED = 'RETRY_FAILED',
|
|
140
|
+
|
|
141
|
+
/** X-Payment-Deadline has already passed */
|
|
142
|
+
DEADLINE_PASSED = 'DEADLINE_PASSED',
|
|
143
|
+
|
|
144
|
+
/** HTTPS protocol required but HTTP used */
|
|
145
|
+
INSECURE_PROTOCOL = 'INSECURE_PROTOCOL',
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Custom error for X402 protocol failures.
|
|
150
|
+
*
|
|
151
|
+
* Provides structured error information for debugging and error handling.
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```typescript
|
|
155
|
+
* try {
|
|
156
|
+
* await x402Adapter.pay({ to: 'https://api.example.com', amount: '10' });
|
|
157
|
+
* } catch (error) {
|
|
158
|
+
* if (error instanceof X402Error) {
|
|
159
|
+
* if (error.code === X402ErrorCode.NETWORK_MISMATCH) {
|
|
160
|
+
* console.error('Wrong network:', error.message);
|
|
161
|
+
* }
|
|
162
|
+
* }
|
|
163
|
+
* }
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
export class X402Error extends Error {
|
|
167
|
+
public readonly name = 'X402Error';
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Creates a new X402Error.
|
|
171
|
+
*
|
|
172
|
+
* @param message - Human-readable error message
|
|
173
|
+
* @param code - Error code for programmatic handling
|
|
174
|
+
* @param response - Optional HTTP response that triggered the error
|
|
175
|
+
*/
|
|
176
|
+
constructor(
|
|
177
|
+
message: string,
|
|
178
|
+
public readonly code: X402ErrorCode,
|
|
179
|
+
public readonly response?: Response
|
|
180
|
+
) {
|
|
181
|
+
super(message);
|
|
182
|
+
|
|
183
|
+
// Maintains proper stack trace in V8 environments
|
|
184
|
+
if (Error.captureStackTrace) {
|
|
185
|
+
Error.captureStackTrace(this, X402Error);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Creates a detailed error message with code.
|
|
191
|
+
*/
|
|
192
|
+
toString(): string {
|
|
193
|
+
return `X402Error [${this.code}]: ${this.message}`;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ============================================================================
|
|
198
|
+
// Type Guards
|
|
199
|
+
// ============================================================================
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Type guard to check if an error is an X402Error.
|
|
203
|
+
*
|
|
204
|
+
* @param error - Error to check
|
|
205
|
+
* @returns True if error is X402Error
|
|
206
|
+
*/
|
|
207
|
+
export function isX402Error(error: unknown): error is X402Error {
|
|
208
|
+
return error instanceof X402Error;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Validates that a network string is a valid X402Network.
|
|
213
|
+
*
|
|
214
|
+
* @param network - Network string to validate
|
|
215
|
+
* @returns True if valid X402Network
|
|
216
|
+
*/
|
|
217
|
+
export function isValidX402Network(network: string): network is X402Network {
|
|
218
|
+
return network === 'base-mainnet' || network === 'base-sepolia';
|
|
219
|
+
}
|
package/src/utils/IPFSClient.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { create, IPFSHTTPClient, Options } from 'kubo-rpc-client';
|
|
7
|
+
import { sdkLogger } from './Logger';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* IPFS Client Interface (from DeliveryProofBuilder)
|
|
@@ -202,10 +203,10 @@ export class IPFSHTTPClientImpl implements IPFSClient {
|
|
|
202
203
|
|
|
203
204
|
// For remote hosts, require HTTPS
|
|
204
205
|
if (!isLocalhost && parsed.protocol !== 'https:') {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
);
|
|
206
|
+
sdkLogger.warn('Using non-HTTPS protocol for remote IPFS endpoint - data may be exposed in transit', {
|
|
207
|
+
protocol: parsed.protocol,
|
|
208
|
+
hostname,
|
|
209
|
+
});
|
|
209
210
|
}
|
|
210
211
|
}
|
|
211
212
|
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { assertSafeFileForRead, ensureSafeDir, ensureSafeFile } from './fsSafe';
|
|
13
|
+
import { sdkLogger } from './Logger';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Maximum allowed nonce value.
|
|
@@ -525,7 +526,7 @@ export class FileBasedNonceManager implements NonceManager {
|
|
|
525
526
|
this.inMemory.recordNonce(messageType, nonce);
|
|
526
527
|
// Fire-and-forget to maintain sync interface
|
|
527
528
|
this.saveToFile().catch((err) => {
|
|
528
|
-
|
|
529
|
+
sdkLogger.error('Failed to save nonce manager state', { error: err instanceof Error ? err.message : String(err) });
|
|
529
530
|
});
|
|
530
531
|
}
|
|
531
532
|
|
|
@@ -537,7 +538,7 @@ export class FileBasedNonceManager implements NonceManager {
|
|
|
537
538
|
this.inMemory.resetNonce(messageType);
|
|
538
539
|
// Fire-and-forget to maintain sync interface
|
|
539
540
|
this.saveToFile().catch((err) => {
|
|
540
|
-
|
|
541
|
+
sdkLogger.error('Failed to save nonce manager state', { error: err instanceof Error ? err.message : String(err) });
|
|
541
542
|
});
|
|
542
543
|
}
|
|
543
544
|
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { assertSafeFileForRead, ensureSafeDir, ensureSafeFile } from './fsSafe';
|
|
15
|
+
import { sdkLogger } from './Logger';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Interface for tracking used attestations
|
|
@@ -205,10 +206,7 @@ export class InMemoryUsedAttestationTracker implements IUsedAttestationTracker {
|
|
|
205
206
|
cleanupOldEntries(_maxAgeHours: number): number {
|
|
206
207
|
// In-memory tracker doesn't track timestamps
|
|
207
208
|
// This is a placeholder for future enhancement
|
|
208
|
-
|
|
209
|
-
'cleanupOldEntries not implemented for InMemoryUsedAttestationTracker. ' +
|
|
210
|
-
'Consider using FileBasedUsedAttestationTracker for time-based cleanup.'
|
|
211
|
-
);
|
|
209
|
+
sdkLogger.warn('cleanupOldEntries not implemented for InMemoryUsedAttestationTracker - use FileBasedUsedAttestationTracker');
|
|
212
210
|
return 0;
|
|
213
211
|
}
|
|
214
212
|
}
|
|
@@ -350,7 +348,7 @@ export class FileBasedUsedAttestationTracker implements IUsedAttestationTracker
|
|
|
350
348
|
const result = this.inMemory.recordUsageSync(attestationUID, txId);
|
|
351
349
|
if (result) {
|
|
352
350
|
this.saveToFile().catch((err) => {
|
|
353
|
-
|
|
351
|
+
sdkLogger.error('Failed to save attestation tracker state', { error: err instanceof Error ? err.message : String(err) });
|
|
354
352
|
});
|
|
355
353
|
}
|
|
356
354
|
return result;
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Circuit Breaker - Gateway Health Tracking (Retry Amplification Protection)
|
|
3
|
+
*
|
|
4
|
+
* Implements the Circuit Breaker pattern for storage gateways:
|
|
5
|
+
* - Tracks consecutive failures per gateway
|
|
6
|
+
* - "Opens" circuit after threshold failures (blocks requests)
|
|
7
|
+
* - Auto-resets after cooldown period
|
|
8
|
+
*
|
|
9
|
+
* This prevents retry amplification attacks where a malicious/failing
|
|
10
|
+
* gateway causes excessive retries.
|
|
11
|
+
*
|
|
12
|
+
* @module utils/circuitBreaker
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Types
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Circuit state
|
|
21
|
+
*/
|
|
22
|
+
export type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Gateway health record
|
|
26
|
+
*/
|
|
27
|
+
interface GatewayHealth {
|
|
28
|
+
/** Consecutive failure count */
|
|
29
|
+
failures: number;
|
|
30
|
+
/** Timestamp of last failure */
|
|
31
|
+
lastFailure: number;
|
|
32
|
+
/** Current circuit state */
|
|
33
|
+
state: CircuitState;
|
|
34
|
+
/** Timestamp when circuit opened */
|
|
35
|
+
openedAt?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Circuit breaker configuration
|
|
40
|
+
*/
|
|
41
|
+
export interface CircuitBreakerConfig {
|
|
42
|
+
/** Number of failures before opening circuit (default: 5) */
|
|
43
|
+
failureThreshold?: number;
|
|
44
|
+
|
|
45
|
+
/** Cooldown period in ms before attempting reset (default: 60000 = 1 min) */
|
|
46
|
+
resetTimeoutMs?: number;
|
|
47
|
+
|
|
48
|
+
/** Time window in ms for counting failures (default: 300000 = 5 min) */
|
|
49
|
+
failureWindowMs?: number;
|
|
50
|
+
|
|
51
|
+
/** Number of successes in half-open to close circuit (default: 2) */
|
|
52
|
+
successThreshold?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// Constants
|
|
57
|
+
// ============================================================================
|
|
58
|
+
|
|
59
|
+
const DEFAULT_FAILURE_THRESHOLD = 5;
|
|
60
|
+
const DEFAULT_RESET_TIMEOUT_MS = 60000; // 1 minute
|
|
61
|
+
const DEFAULT_FAILURE_WINDOW_MS = 300000; // 5 minutes
|
|
62
|
+
const DEFAULT_SUCCESS_THRESHOLD = 2;
|
|
63
|
+
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// GatewayCircuitBreaker Class
|
|
66
|
+
// ============================================================================
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Circuit Breaker for gateway health management
|
|
70
|
+
*
|
|
71
|
+
* Usage:
|
|
72
|
+
* ```typescript
|
|
73
|
+
* const circuitBreaker = new GatewayCircuitBreaker();
|
|
74
|
+
*
|
|
75
|
+
* // Before making request
|
|
76
|
+
* if (!circuitBreaker.isHealthy(gatewayUrl)) {
|
|
77
|
+
* throw new Error('Gateway circuit open');
|
|
78
|
+
* }
|
|
79
|
+
*
|
|
80
|
+
* try {
|
|
81
|
+
* const result = await fetchFromGateway(gatewayUrl);
|
|
82
|
+
* circuitBreaker.recordSuccess(gatewayUrl);
|
|
83
|
+
* return result;
|
|
84
|
+
* } catch (error) {
|
|
85
|
+
* circuitBreaker.recordFailure(gatewayUrl);
|
|
86
|
+
* throw error;
|
|
87
|
+
* }
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export class GatewayCircuitBreaker {
|
|
91
|
+
private readonly health = new Map<string, GatewayHealth>();
|
|
92
|
+
private readonly config: Required<CircuitBreakerConfig>;
|
|
93
|
+
private halfOpenSuccesses = new Map<string, number>();
|
|
94
|
+
|
|
95
|
+
constructor(config: CircuitBreakerConfig = {}) {
|
|
96
|
+
this.config = {
|
|
97
|
+
failureThreshold: config.failureThreshold ?? DEFAULT_FAILURE_THRESHOLD,
|
|
98
|
+
resetTimeoutMs: config.resetTimeoutMs ?? DEFAULT_RESET_TIMEOUT_MS,
|
|
99
|
+
failureWindowMs: config.failureWindowMs ?? DEFAULT_FAILURE_WINDOW_MS,
|
|
100
|
+
successThreshold: config.successThreshold ?? DEFAULT_SUCCESS_THRESHOLD
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Check if gateway is healthy (circuit closed or half-open)
|
|
106
|
+
*
|
|
107
|
+
* @param url - Gateway URL
|
|
108
|
+
* @returns true if requests should be allowed
|
|
109
|
+
*/
|
|
110
|
+
isHealthy(url: string): boolean {
|
|
111
|
+
const state = this.getState(url);
|
|
112
|
+
return state !== 'OPEN';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get current circuit state for gateway
|
|
117
|
+
*
|
|
118
|
+
* @param url - Gateway URL
|
|
119
|
+
* @returns Circuit state
|
|
120
|
+
*/
|
|
121
|
+
getState(url: string): CircuitState {
|
|
122
|
+
const health = this.health.get(url);
|
|
123
|
+
|
|
124
|
+
if (!health) {
|
|
125
|
+
return 'CLOSED';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Check if circuit should transition from OPEN to HALF_OPEN
|
|
129
|
+
if (health.state === 'OPEN') {
|
|
130
|
+
const timeSinceOpen = Date.now() - (health.openedAt || 0);
|
|
131
|
+
if (timeSinceOpen >= this.config.resetTimeoutMs) {
|
|
132
|
+
// Transition to half-open
|
|
133
|
+
health.state = 'HALF_OPEN';
|
|
134
|
+
this.halfOpenSuccesses.set(url, 0);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check if failures have expired (outside window)
|
|
139
|
+
if (health.state === 'CLOSED') {
|
|
140
|
+
const timeSinceLastFailure = Date.now() - health.lastFailure;
|
|
141
|
+
if (timeSinceLastFailure >= this.config.failureWindowMs) {
|
|
142
|
+
// Reset failure count
|
|
143
|
+
health.failures = 0;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return health.state;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Record a successful request
|
|
152
|
+
*
|
|
153
|
+
* @param url - Gateway URL
|
|
154
|
+
*/
|
|
155
|
+
recordSuccess(url: string): void {
|
|
156
|
+
const health = this.health.get(url);
|
|
157
|
+
|
|
158
|
+
if (!health) {
|
|
159
|
+
return; // No failures recorded, nothing to do
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (health.state === 'HALF_OPEN') {
|
|
163
|
+
// Count successes in half-open state
|
|
164
|
+
const successes = (this.halfOpenSuccesses.get(url) || 0) + 1;
|
|
165
|
+
this.halfOpenSuccesses.set(url, successes);
|
|
166
|
+
|
|
167
|
+
if (successes >= this.config.successThreshold) {
|
|
168
|
+
// Enough successes - close circuit
|
|
169
|
+
this.reset(url);
|
|
170
|
+
}
|
|
171
|
+
} else if (health.state === 'CLOSED') {
|
|
172
|
+
// Reset failure count on success
|
|
173
|
+
health.failures = 0;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Record a failed request
|
|
179
|
+
*
|
|
180
|
+
* @param url - Gateway URL
|
|
181
|
+
*/
|
|
182
|
+
recordFailure(url: string): void {
|
|
183
|
+
const now = Date.now();
|
|
184
|
+
let health = this.health.get(url);
|
|
185
|
+
|
|
186
|
+
if (!health) {
|
|
187
|
+
health = {
|
|
188
|
+
failures: 0,
|
|
189
|
+
lastFailure: now,
|
|
190
|
+
state: 'CLOSED'
|
|
191
|
+
};
|
|
192
|
+
this.health.set(url, health);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// If in half-open state, any failure opens circuit again
|
|
196
|
+
if (health.state === 'HALF_OPEN') {
|
|
197
|
+
health.state = 'OPEN';
|
|
198
|
+
health.openedAt = now;
|
|
199
|
+
health.failures = this.config.failureThreshold; // Keep at threshold
|
|
200
|
+
this.halfOpenSuccesses.delete(url);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Check if previous failures have expired
|
|
205
|
+
const timeSinceLastFailure = now - health.lastFailure;
|
|
206
|
+
if (timeSinceLastFailure >= this.config.failureWindowMs) {
|
|
207
|
+
health.failures = 0;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Increment failure count
|
|
211
|
+
health.failures++;
|
|
212
|
+
health.lastFailure = now;
|
|
213
|
+
|
|
214
|
+
// Check if threshold reached
|
|
215
|
+
if (health.failures >= this.config.failureThreshold) {
|
|
216
|
+
health.state = 'OPEN';
|
|
217
|
+
health.openedAt = now;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Reset circuit for gateway (force close)
|
|
223
|
+
*
|
|
224
|
+
* @param url - Gateway URL
|
|
225
|
+
*/
|
|
226
|
+
reset(url: string): void {
|
|
227
|
+
this.health.delete(url);
|
|
228
|
+
this.halfOpenSuccesses.delete(url);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Reset all circuits
|
|
233
|
+
*/
|
|
234
|
+
resetAll(): void {
|
|
235
|
+
this.health.clear();
|
|
236
|
+
this.halfOpenSuccesses.clear();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Get health status for all tracked gateways
|
|
241
|
+
*
|
|
242
|
+
* @returns Map of gateway URL to health info
|
|
243
|
+
*/
|
|
244
|
+
getHealthStatus(): Map<string, { state: CircuitState; failures: number }> {
|
|
245
|
+
const status = new Map<string, { state: CircuitState; failures: number }>();
|
|
246
|
+
|
|
247
|
+
for (const [url, health] of this.health) {
|
|
248
|
+
status.set(url, {
|
|
249
|
+
state: this.getState(url), // This updates state if needed
|
|
250
|
+
failures: health.failures
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return status;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Get failure count for gateway
|
|
259
|
+
*
|
|
260
|
+
* @param url - Gateway URL
|
|
261
|
+
* @returns Number of consecutive failures
|
|
262
|
+
*/
|
|
263
|
+
getFailureCount(url: string): number {
|
|
264
|
+
return this.health.get(url)?.failures || 0;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ============================================================================
|
|
269
|
+
// Singleton Instance
|
|
270
|
+
// ============================================================================
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Default global circuit breaker instance
|
|
274
|
+
*
|
|
275
|
+
* Use this for simple cases where a single circuit breaker is sufficient.
|
|
276
|
+
* For more control, create your own GatewayCircuitBreaker instance.
|
|
277
|
+
*/
|
|
278
|
+
export const globalCircuitBreaker = new GatewayCircuitBreaker();
|
|
279
|
+
|
|
280
|
+
// ============================================================================
|
|
281
|
+
// Helper Functions
|
|
282
|
+
// ============================================================================
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Wrap an async operation with circuit breaker protection
|
|
286
|
+
*
|
|
287
|
+
* @param url - Gateway URL to track
|
|
288
|
+
* @param operation - Async operation to execute
|
|
289
|
+
* @param circuitBreaker - Circuit breaker instance (default: global)
|
|
290
|
+
* @returns Operation result
|
|
291
|
+
* @throws Error if circuit is open, or operation error
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* ```typescript
|
|
295
|
+
* const result = await withCircuitBreaker(
|
|
296
|
+
* 'https://ipfs.filebase.io',
|
|
297
|
+
* () => fetch(url).then(r => r.json())
|
|
298
|
+
* );
|
|
299
|
+
* ```
|
|
300
|
+
*/
|
|
301
|
+
export async function withCircuitBreaker<T>(
|
|
302
|
+
url: string,
|
|
303
|
+
operation: () => Promise<T>,
|
|
304
|
+
circuitBreaker: GatewayCircuitBreaker = globalCircuitBreaker
|
|
305
|
+
): Promise<T> {
|
|
306
|
+
// Check if circuit allows request
|
|
307
|
+
if (!circuitBreaker.isHealthy(url)) {
|
|
308
|
+
const state = circuitBreaker.getState(url);
|
|
309
|
+
throw new Error(
|
|
310
|
+
`Circuit breaker OPEN for gateway: ${url}. ` +
|
|
311
|
+
`State: ${state}, Failures: ${circuitBreaker.getFailureCount(url)}. ` +
|
|
312
|
+
`Please wait for cooldown period before retrying.`
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
const result = await operation();
|
|
318
|
+
circuitBreaker.recordSuccess(url);
|
|
319
|
+
return result;
|
|
320
|
+
} catch (error) {
|
|
321
|
+
circuitBreaker.recordFailure(url);
|
|
322
|
+
throw error;
|
|
323
|
+
}
|
|
324
|
+
}
|