@agirails/sdk 2.0.0-beta
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 +183 -0
- package/dist/ACTPClient.d.ts +52 -0
- package/dist/ACTPClient.d.ts.map +1 -0
- package/dist/ACTPClient.js +120 -0
- package/dist/ACTPClient.js.map +1 -0
- package/dist/abi/ACTPKernel.json +1340 -0
- package/dist/abi/ERC20.json +38 -0
- package/dist/abi/EscrowVault.json +64 -0
- package/dist/builders/DeliveryProofBuilder.d.ts +37 -0
- package/dist/builders/DeliveryProofBuilder.d.ts.map +1 -0
- package/dist/builders/DeliveryProofBuilder.js +165 -0
- package/dist/builders/DeliveryProofBuilder.js.map +1 -0
- package/dist/builders/QuoteBuilder.d.ts +68 -0
- package/dist/builders/QuoteBuilder.d.ts.map +1 -0
- package/dist/builders/QuoteBuilder.js +255 -0
- package/dist/builders/QuoteBuilder.js.map +1 -0
- package/dist/builders/index.d.ts +3 -0
- package/dist/builders/index.d.ts.map +1 -0
- package/dist/builders/index.js +10 -0
- package/dist/builders/index.js.map +1 -0
- package/dist/config/networks.d.ts +27 -0
- package/dist/config/networks.d.ts.map +1 -0
- package/dist/config/networks.js +103 -0
- package/dist/config/networks.js.map +1 -0
- package/dist/errors/index.d.ts +38 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +87 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +68 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol/ACTPKernel.d.ts +30 -0
- package/dist/protocol/ACTPKernel.d.ts.map +1 -0
- package/dist/protocol/ACTPKernel.js +261 -0
- package/dist/protocol/ACTPKernel.js.map +1 -0
- package/dist/protocol/EASHelper.d.ts +23 -0
- package/dist/protocol/EASHelper.d.ts.map +1 -0
- package/dist/protocol/EASHelper.js +106 -0
- package/dist/protocol/EASHelper.js.map +1 -0
- package/dist/protocol/EscrowVault.d.ts +24 -0
- package/dist/protocol/EscrowVault.d.ts.map +1 -0
- package/dist/protocol/EscrowVault.js +114 -0
- package/dist/protocol/EscrowVault.js.map +1 -0
- package/dist/protocol/EventMonitor.d.ts +18 -0
- package/dist/protocol/EventMonitor.d.ts.map +1 -0
- package/dist/protocol/EventMonitor.js +92 -0
- package/dist/protocol/EventMonitor.js.map +1 -0
- package/dist/protocol/MessageSigner.d.ts +23 -0
- package/dist/protocol/MessageSigner.d.ts.map +1 -0
- package/dist/protocol/MessageSigner.js +178 -0
- package/dist/protocol/MessageSigner.js.map +1 -0
- package/dist/protocol/ProofGenerator.d.ts +22 -0
- package/dist/protocol/ProofGenerator.d.ts.map +1 -0
- package/dist/protocol/ProofGenerator.js +64 -0
- package/dist/protocol/ProofGenerator.js.map +1 -0
- package/dist/protocol/QuoteBuilder.d.ts +2 -0
- package/dist/protocol/QuoteBuilder.d.ts.map +1 -0
- package/dist/protocol/QuoteBuilder.js +7 -0
- package/dist/protocol/QuoteBuilder.js.map +1 -0
- package/dist/types/eip712.d.ts +106 -0
- package/dist/types/eip712.d.ts.map +1 -0
- package/dist/types/eip712.js +84 -0
- package/dist/types/eip712.js.map +1 -0
- package/dist/types/escrow.d.ts +18 -0
- package/dist/types/escrow.d.ts.map +1 -0
- package/dist/types/escrow.js +3 -0
- package/dist/types/escrow.js.map +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +22 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/message.d.ts +109 -0
- package/dist/types/message.d.ts.map +1 -0
- package/dist/types/message.js +3 -0
- package/dist/types/message.js.map +1 -0
- package/dist/types/state.d.ts +19 -0
- package/dist/types/state.d.ts.map +1 -0
- package/dist/types/state.js +49 -0
- package/dist/types/state.js.map +1 -0
- package/dist/types/transaction.d.ts +36 -0
- package/dist/types/transaction.d.ts.map +1 -0
- package/dist/types/transaction.js +3 -0
- package/dist/types/transaction.js.map +1 -0
- package/dist/utils/IPFSClient.d.ts +37 -0
- package/dist/utils/IPFSClient.d.ts.map +1 -0
- package/dist/utils/IPFSClient.js +128 -0
- package/dist/utils/IPFSClient.js.map +1 -0
- package/dist/utils/NonceManager.d.ts +34 -0
- package/dist/utils/NonceManager.d.ts.map +1 -0
- package/dist/utils/NonceManager.js +114 -0
- package/dist/utils/NonceManager.js.map +1 -0
- package/dist/utils/ReceivedNonceTracker.d.ts +35 -0
- package/dist/utils/ReceivedNonceTracker.d.ts.map +1 -0
- package/dist/utils/ReceivedNonceTracker.js +196 -0
- package/dist/utils/ReceivedNonceTracker.js.map +1 -0
- package/dist/utils/canonicalJson.d.ts +4 -0
- package/dist/utils/canonicalJson.d.ts.map +1 -0
- package/dist/utils/canonicalJson.js +21 -0
- package/dist/utils/canonicalJson.js.map +1 -0
- package/dist/utils/computeTypeHash.d.ts +3 -0
- package/dist/utils/computeTypeHash.d.ts.map +1 -0
- package/dist/utils/computeTypeHash.js +30 -0
- package/dist/utils/computeTypeHash.js.map +1 -0
- package/dist/utils/validation.d.ts +6 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +46 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +73 -0
- package/src/ACTPClient.ts +276 -0
- package/src/__tests__/ProofGenerator.test.ts +124 -0
- package/src/__tests__/QuoteBuilder.test.ts +516 -0
- package/src/__tests__/StateMachine.test.ts +82 -0
- package/src/__tests__/builders/DeliveryProofBuilder.test.ts +581 -0
- package/src/__tests__/integration/ACTPClient.test.ts +263 -0
- package/src/__tests__/integration.test.ts +289 -0
- package/src/__tests__/protocol/EASHelper.test.ts +472 -0
- package/src/__tests__/protocol/EventMonitor.test.ts +382 -0
- package/src/__tests__/security/ACTPKernel.security.test.ts +1167 -0
- package/src/__tests__/security/EscrowVault.security.test.ts +570 -0
- package/src/__tests__/security/MessageSigner.security.test.ts +286 -0
- package/src/__tests__/security/NonceReplay.security.test.ts +501 -0
- package/src/__tests__/security/validation.security.test.ts +376 -0
- package/src/__tests__/utils/IPFSClient.test.ts +262 -0
- package/src/__tests__/utils/NonceManager.test.ts +205 -0
- package/src/__tests__/utils/canonicalJson.test.ts +153 -0
- package/src/abi/ACTPKernel.json +1340 -0
- package/src/abi/ERC20.json +40 -0
- package/src/abi/EscrowVault.json +66 -0
- package/src/builders/DeliveryProofBuilder.ts +326 -0
- package/src/builders/QuoteBuilder.ts +483 -0
- package/src/builders/index.ts +17 -0
- package/src/config/networks.ts +165 -0
- package/src/errors/index.ts +130 -0
- package/src/index.ts +108 -0
- package/src/protocol/ACTPKernel.ts +625 -0
- package/src/protocol/EASHelper.ts +197 -0
- package/src/protocol/EscrowVault.ts +237 -0
- package/src/protocol/EventMonitor.ts +161 -0
- package/src/protocol/MessageSigner.ts +336 -0
- package/src/protocol/ProofGenerator.ts +119 -0
- package/src/protocol/QuoteBuilder.ts +15 -0
- package/src/types/eip712.ts +175 -0
- package/src/types/escrow.ts +26 -0
- package/src/types/index.ts +10 -0
- package/src/types/message.ts +145 -0
- package/src/types/state.ts +77 -0
- package/src/types/transaction.ts +54 -0
- package/src/utils/IPFSClient.ts +248 -0
- package/src/utils/NonceManager.ts +293 -0
- package/src/utils/ReceivedNonceTracker.ts +397 -0
- package/src/utils/canonicalJson.ts +38 -0
- package/src/utils/computeTypeHash.ts +50 -0
- package/src/utils/validation.ts +82 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nonce Manager Implementation
|
|
3
|
+
* Tracks nonces per DID + message type for AIP-4 delivery proofs
|
|
4
|
+
* Reference: AIP-4 §3.2 (nonce field requirement)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Nonce Manager Interface (from DeliveryProofBuilder)
|
|
9
|
+
*/
|
|
10
|
+
export interface NonceManager {
|
|
11
|
+
/**
|
|
12
|
+
* Get next nonce for message type
|
|
13
|
+
* @param messageType - Message type identifier (e.g., "agirails.delivery.v1")
|
|
14
|
+
* @returns Monotonically increasing nonce
|
|
15
|
+
*/
|
|
16
|
+
getNextNonce(messageType: string): number;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Record nonce usage
|
|
20
|
+
* @param messageType - Message type identifier
|
|
21
|
+
* @param nonce - Nonce used
|
|
22
|
+
*/
|
|
23
|
+
recordNonce(messageType: string, nonce: number): void;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get current nonce (last used)
|
|
27
|
+
* @param messageType - Message type identifier
|
|
28
|
+
* @returns Current nonce or 0 if none used
|
|
29
|
+
*/
|
|
30
|
+
getCurrentNonce(messageType: string): number;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Reset nonce for message type
|
|
34
|
+
* @param messageType - Message type identifier
|
|
35
|
+
*/
|
|
36
|
+
resetNonce(messageType: string): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* In-Memory Nonce Manager
|
|
41
|
+
* Simple implementation using Map for per-message-type nonce tracking
|
|
42
|
+
*
|
|
43
|
+
* ⚠️ WARNING: Nonces are lost on process restart. For production:
|
|
44
|
+
* - Use persistent storage (Redis, PostgreSQL, etc.)
|
|
45
|
+
* - Implement nonce recovery from blockchain events
|
|
46
|
+
* - Add DID-scoped nonce tracking
|
|
47
|
+
*/
|
|
48
|
+
export class InMemoryNonceManager implements NonceManager {
|
|
49
|
+
private nonces: Map<string, number> = new Map();
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Create in-memory nonce manager
|
|
53
|
+
* @param initialNonces - Optional initial nonce values (for recovery)
|
|
54
|
+
*/
|
|
55
|
+
constructor(initialNonces?: Record<string, number>) {
|
|
56
|
+
if (initialNonces) {
|
|
57
|
+
Object.entries(initialNonces).forEach(([messageType, nonce]) => {
|
|
58
|
+
this.nonces.set(messageType, nonce);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get next nonce for message type
|
|
65
|
+
* @param messageType - Message type identifier
|
|
66
|
+
* @returns Monotonically increasing nonce
|
|
67
|
+
*/
|
|
68
|
+
getNextNonce(messageType: string): number {
|
|
69
|
+
const current = this.nonces.get(messageType) || 0;
|
|
70
|
+
return current + 1;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Record nonce usage
|
|
75
|
+
* @param messageType - Message type identifier
|
|
76
|
+
* @param nonce - Nonce used
|
|
77
|
+
*/
|
|
78
|
+
recordNonce(messageType: string, nonce: number): void {
|
|
79
|
+
const current = this.nonces.get(messageType) || 0;
|
|
80
|
+
|
|
81
|
+
// Ensure monotonic increase
|
|
82
|
+
if (nonce <= current) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Nonce must be strictly increasing: attempted ${nonce}, current is ${current}`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this.nonces.set(messageType, nonce);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get current nonce (last used)
|
|
93
|
+
* @param messageType - Message type identifier
|
|
94
|
+
* @returns Current nonce or 0 if none used
|
|
95
|
+
*/
|
|
96
|
+
getCurrentNonce(messageType: string): number {
|
|
97
|
+
return this.nonces.get(messageType) || 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Reset nonce for message type
|
|
102
|
+
* @param messageType - Message type identifier
|
|
103
|
+
*/
|
|
104
|
+
resetNonce(messageType: string): void {
|
|
105
|
+
this.nonces.delete(messageType);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get all nonces (for persistence)
|
|
110
|
+
* @returns Record of all message type nonces
|
|
111
|
+
*/
|
|
112
|
+
getAllNonces(): Record<string, number> {
|
|
113
|
+
return Object.fromEntries(this.nonces.entries());
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Clear all nonces
|
|
118
|
+
*/
|
|
119
|
+
clearAll(): void {
|
|
120
|
+
this.nonces.clear();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* DID-Scoped Nonce Manager
|
|
126
|
+
* Tracks nonces per DID + message type combination
|
|
127
|
+
* Recommended for multi-agent scenarios
|
|
128
|
+
*/
|
|
129
|
+
export class DIDScopedNonceManager implements NonceManager {
|
|
130
|
+
private nonces: Map<string, Map<string, number>> = new Map();
|
|
131
|
+
private currentDID: string;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Create DID-scoped nonce manager
|
|
135
|
+
* @param did - DID to track nonces for
|
|
136
|
+
* @param initialNonces - Optional initial nonce values
|
|
137
|
+
*/
|
|
138
|
+
constructor(did: string, initialNonces?: Record<string, number>) {
|
|
139
|
+
this.currentDID = did;
|
|
140
|
+
|
|
141
|
+
if (initialNonces) {
|
|
142
|
+
const didNonces = new Map<string, number>();
|
|
143
|
+
Object.entries(initialNonces).forEach(([messageType, nonce]) => {
|
|
144
|
+
didNonces.set(messageType, nonce);
|
|
145
|
+
});
|
|
146
|
+
this.nonces.set(did, didNonces);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get next nonce for message type (current DID)
|
|
152
|
+
* @param messageType - Message type identifier
|
|
153
|
+
* @returns Monotonically increasing nonce
|
|
154
|
+
*/
|
|
155
|
+
getNextNonce(messageType: string): number {
|
|
156
|
+
return this.getNextNonceForDID(this.currentDID, messageType);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Record nonce usage (current DID)
|
|
161
|
+
* @param messageType - Message type identifier
|
|
162
|
+
* @param nonce - Nonce used
|
|
163
|
+
*/
|
|
164
|
+
recordNonce(messageType: string, nonce: number): void {
|
|
165
|
+
this.recordNonceForDID(this.currentDID, messageType, nonce);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get current nonce (current DID)
|
|
170
|
+
* @param messageType - Message type identifier
|
|
171
|
+
* @returns Current nonce or 0 if none used
|
|
172
|
+
*/
|
|
173
|
+
getCurrentNonce(messageType: string): number {
|
|
174
|
+
return this.getCurrentNonceForDID(this.currentDID, messageType);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Reset nonce for message type (current DID)
|
|
179
|
+
* @param messageType - Message type identifier
|
|
180
|
+
*/
|
|
181
|
+
resetNonce(messageType: string): void {
|
|
182
|
+
this.resetNonceForDID(this.currentDID, messageType);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get next nonce for specific DID + message type
|
|
187
|
+
* @param did - DID identifier
|
|
188
|
+
* @param messageType - Message type identifier
|
|
189
|
+
* @returns Monotonically increasing nonce
|
|
190
|
+
*/
|
|
191
|
+
getNextNonceForDID(did: string, messageType: string): number {
|
|
192
|
+
const didNonces = this.nonces.get(did);
|
|
193
|
+
if (!didNonces) {
|
|
194
|
+
return 1;
|
|
195
|
+
}
|
|
196
|
+
const current = didNonces.get(messageType) || 0;
|
|
197
|
+
return current + 1;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Record nonce usage for specific DID + message type
|
|
202
|
+
* @param did - DID identifier
|
|
203
|
+
* @param messageType - Message type identifier
|
|
204
|
+
* @param nonce - Nonce used
|
|
205
|
+
*/
|
|
206
|
+
recordNonceForDID(did: string, messageType: string, nonce: number): void {
|
|
207
|
+
let didNonces = this.nonces.get(did);
|
|
208
|
+
|
|
209
|
+
if (!didNonces) {
|
|
210
|
+
didNonces = new Map<string, number>();
|
|
211
|
+
this.nonces.set(did, didNonces);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const current = didNonces.get(messageType) || 0;
|
|
215
|
+
|
|
216
|
+
// Ensure monotonic increase
|
|
217
|
+
if (nonce <= current) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
`Nonce must be strictly increasing for ${did}: attempted ${nonce}, current is ${current}`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
didNonces.set(messageType, nonce);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Get current nonce for specific DID + message type
|
|
228
|
+
* @param did - DID identifier
|
|
229
|
+
* @param messageType - Message type identifier
|
|
230
|
+
* @returns Current nonce or 0 if none used
|
|
231
|
+
*/
|
|
232
|
+
getCurrentNonceForDID(did: string, messageType: string): number {
|
|
233
|
+
const didNonces = this.nonces.get(did);
|
|
234
|
+
return didNonces?.get(messageType) || 0;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Reset nonce for specific DID + message type
|
|
239
|
+
* @param did - DID identifier
|
|
240
|
+
* @param messageType - Message type identifier
|
|
241
|
+
*/
|
|
242
|
+
resetNonceForDID(did: string, messageType: string): void {
|
|
243
|
+
const didNonces = this.nonces.get(did);
|
|
244
|
+
if (didNonces) {
|
|
245
|
+
didNonces.delete(messageType);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Switch current DID context
|
|
251
|
+
* @param did - New DID to track
|
|
252
|
+
*/
|
|
253
|
+
switchDID(did: string): void {
|
|
254
|
+
this.currentDID = did;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Get all nonces for all DIDs (for persistence)
|
|
259
|
+
* @returns Nested record of DID → message type → nonce
|
|
260
|
+
*/
|
|
261
|
+
getAllNonces(): Record<string, Record<string, number>> {
|
|
262
|
+
const result: Record<string, Record<string, number>> = {};
|
|
263
|
+
|
|
264
|
+
this.nonces.forEach((didNonces, did) => {
|
|
265
|
+
result[did] = Object.fromEntries(didNonces.entries());
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Clear all nonces for all DIDs
|
|
273
|
+
*/
|
|
274
|
+
clearAll(): void {
|
|
275
|
+
this.nonces.clear();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Create nonce manager based on environment
|
|
281
|
+
* @param did - Optional DID for scoped tracking
|
|
282
|
+
* @param initialNonces - Optional initial nonces
|
|
283
|
+
* @returns NonceManager instance
|
|
284
|
+
*/
|
|
285
|
+
export function createNonceManager(
|
|
286
|
+
did?: string,
|
|
287
|
+
initialNonces?: Record<string, number>
|
|
288
|
+
): NonceManager {
|
|
289
|
+
if (did) {
|
|
290
|
+
return new DIDScopedNonceManager(did, initialNonces);
|
|
291
|
+
}
|
|
292
|
+
return new InMemoryNonceManager(initialNonces);
|
|
293
|
+
}
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReceivedNonceTracker - Replay Attack Prevention for Message Receivers
|
|
3
|
+
*
|
|
4
|
+
* This utility tracks nonces of received messages to prevent replay attacks.
|
|
5
|
+
* It works in conjunction with NonceManager (for senders) but serves the receiver side.
|
|
6
|
+
*
|
|
7
|
+
* Reference: V4 Security Vulnerability (EIP-712 Replay Attack)
|
|
8
|
+
*
|
|
9
|
+
* Usage Pattern:
|
|
10
|
+
* - Sender: Uses NonceManager to generate monotonically increasing nonces
|
|
11
|
+
* - Receiver: Uses ReceivedNonceTracker to validate and track received nonces
|
|
12
|
+
*
|
|
13
|
+
* Security Properties:
|
|
14
|
+
* 1. Nonces must be monotonically increasing per sender + message type
|
|
15
|
+
* 2. Duplicate nonces are rejected (replay attack prevention)
|
|
16
|
+
* 3. Nonces that are lower than the highest seen are rejected (old replay prevention)
|
|
17
|
+
*
|
|
18
|
+
* ⚠️ WARNING: In-memory tracking only. For production:
|
|
19
|
+
* - Use persistent storage (Redis, PostgreSQL, etc.)
|
|
20
|
+
* - Implement nonce recovery from transaction history
|
|
21
|
+
* - Consider nonce expiry for long-running processes
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Nonce validation result
|
|
26
|
+
*/
|
|
27
|
+
export interface NonceValidationResult {
|
|
28
|
+
valid: boolean;
|
|
29
|
+
reason?: string;
|
|
30
|
+
expectedMinimum?: string; // bytes32 format
|
|
31
|
+
receivedNonce?: string; // bytes32 format
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Interface for tracking received nonces
|
|
36
|
+
*/
|
|
37
|
+
export interface IReceivedNonceTracker {
|
|
38
|
+
/**
|
|
39
|
+
* Validate and record a received nonce
|
|
40
|
+
* @param sender - Sender DID (e.g., "did:ethr:0x...")
|
|
41
|
+
* @param messageType - Message type (e.g., "agirails.delivery.v1")
|
|
42
|
+
* @param nonce - Nonce value (bytes32 format: "0x...")
|
|
43
|
+
* @returns Validation result
|
|
44
|
+
*/
|
|
45
|
+
validateAndRecord(sender: string, messageType: string, nonce: string): NonceValidationResult;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if a nonce has been used (without recording)
|
|
49
|
+
* @param sender - Sender DID
|
|
50
|
+
* @param messageType - Message type
|
|
51
|
+
* @param nonce - Nonce value (bytes32 format)
|
|
52
|
+
* @returns true if nonce was already used
|
|
53
|
+
*/
|
|
54
|
+
hasBeenUsed(sender: string, messageType: string, nonce: string): boolean;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get highest nonce seen for sender + message type
|
|
58
|
+
* @param sender - Sender DID
|
|
59
|
+
* @param messageType - Message type
|
|
60
|
+
* @returns Highest nonce (bytes32 format) or null if none seen
|
|
61
|
+
*/
|
|
62
|
+
getHighestNonce(sender: string, messageType: string): string | null;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Reset tracking for a specific sender + message type
|
|
66
|
+
* @param sender - Sender DID
|
|
67
|
+
* @param messageType - Message type
|
|
68
|
+
*/
|
|
69
|
+
reset(sender: string, messageType: string): void;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Clear all tracked nonces
|
|
73
|
+
*/
|
|
74
|
+
clearAll(): void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* In-Memory Received Nonce Tracker
|
|
79
|
+
*
|
|
80
|
+
* Strategy: Track highest nonce seen per sender + message type
|
|
81
|
+
* - Accept nonces that are strictly greater than the highest seen
|
|
82
|
+
* - Reject nonces that are <= highest seen (replay attack)
|
|
83
|
+
*
|
|
84
|
+
* Trade-off:
|
|
85
|
+
* - Memory efficient (one value per sender + type)
|
|
86
|
+
* - Requires ordered nonce sequences
|
|
87
|
+
* - Cannot skip nonces (nonce gaps are rejected)
|
|
88
|
+
*/
|
|
89
|
+
export class InMemoryReceivedNonceTracker implements IReceivedNonceTracker {
|
|
90
|
+
// Map: sender -> messageType -> highest nonce (as BigInt for comparison)
|
|
91
|
+
private highestNonces: Map<string, Map<string, bigint>> = new Map();
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Validate and record a received nonce
|
|
95
|
+
*/
|
|
96
|
+
validateAndRecord(sender: string, messageType: string, nonce: string): NonceValidationResult {
|
|
97
|
+
// Validate nonce format (must be bytes32: 0x + 64 hex chars)
|
|
98
|
+
if (!/^0x[a-fA-F0-9]{64}$/.test(nonce)) {
|
|
99
|
+
return {
|
|
100
|
+
valid: false,
|
|
101
|
+
reason: 'Invalid nonce format (must be bytes32)',
|
|
102
|
+
receivedNonce: nonce
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Convert nonce to BigInt for comparison
|
|
107
|
+
const nonceValue = BigInt(nonce);
|
|
108
|
+
|
|
109
|
+
// Get sender's nonce map
|
|
110
|
+
let senderNonces = this.highestNonces.get(sender);
|
|
111
|
+
if (!senderNonces) {
|
|
112
|
+
senderNonces = new Map<string, bigint>();
|
|
113
|
+
this.highestNonces.set(sender, senderNonces);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Get highest nonce for this message type
|
|
117
|
+
const highestNonce = senderNonces.get(messageType);
|
|
118
|
+
|
|
119
|
+
if (highestNonce === undefined) {
|
|
120
|
+
// First message from this sender for this type
|
|
121
|
+
senderNonces.set(messageType, nonceValue);
|
|
122
|
+
return { valid: true };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Nonce must be strictly greater than highest seen
|
|
126
|
+
if (nonceValue <= highestNonce) {
|
|
127
|
+
const expectedMinimum = '0x' + (highestNonce + BigInt(1)).toString(16).padStart(64, '0');
|
|
128
|
+
return {
|
|
129
|
+
valid: false,
|
|
130
|
+
reason: `Nonce replay detected: nonce must be > ${this.bigintToBytes32(highestNonce)}`,
|
|
131
|
+
expectedMinimum,
|
|
132
|
+
receivedNonce: nonce
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Valid nonce - record it
|
|
137
|
+
senderNonces.set(messageType, nonceValue);
|
|
138
|
+
return { valid: true };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Check if a nonce has been used (non-mutating)
|
|
143
|
+
*/
|
|
144
|
+
hasBeenUsed(sender: string, messageType: string, nonce: string): boolean {
|
|
145
|
+
const nonceValue = BigInt(nonce);
|
|
146
|
+
const senderNonces = this.highestNonces.get(sender);
|
|
147
|
+
|
|
148
|
+
if (!senderNonces) {
|
|
149
|
+
return false; // No nonces seen from this sender
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const highestNonce = senderNonces.get(messageType);
|
|
153
|
+
if (highestNonce === undefined) {
|
|
154
|
+
return false; // No nonces seen for this message type
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// If the provided nonce is <= highest seen, it's been "used"
|
|
158
|
+
return nonceValue <= highestNonce;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get highest nonce seen
|
|
163
|
+
*/
|
|
164
|
+
getHighestNonce(sender: string, messageType: string): string | null {
|
|
165
|
+
const senderNonces = this.highestNonces.get(sender);
|
|
166
|
+
if (!senderNonces) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const highestNonce = senderNonces.get(messageType);
|
|
171
|
+
if (highestNonce === undefined) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return this.bigintToBytes32(highestNonce);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Reset tracking for sender + message type
|
|
180
|
+
*/
|
|
181
|
+
reset(sender: string, messageType: string): void {
|
|
182
|
+
const senderNonces = this.highestNonces.get(sender);
|
|
183
|
+
if (senderNonces) {
|
|
184
|
+
senderNonces.delete(messageType);
|
|
185
|
+
// Clean up sender map if empty
|
|
186
|
+
if (senderNonces.size === 0) {
|
|
187
|
+
this.highestNonces.delete(sender);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Clear all tracked nonces
|
|
194
|
+
*/
|
|
195
|
+
clearAll(): void {
|
|
196
|
+
this.highestNonces.clear();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Convert BigInt to bytes32 hex string
|
|
201
|
+
*/
|
|
202
|
+
private bigintToBytes32(value: bigint): string {
|
|
203
|
+
return '0x' + value.toString(16).padStart(64, '0');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get all nonces (for debugging/persistence)
|
|
208
|
+
*/
|
|
209
|
+
getAllNonces(): Record<string, Record<string, string>> {
|
|
210
|
+
const result: Record<string, Record<string, string>> = {};
|
|
211
|
+
|
|
212
|
+
this.highestNonces.forEach((senderNonces, sender) => {
|
|
213
|
+
result[sender] = {};
|
|
214
|
+
senderNonces.forEach((nonce, messageType) => {
|
|
215
|
+
result[sender][messageType] = this.bigintToBytes32(nonce);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Set-Based Received Nonce Tracker
|
|
225
|
+
*
|
|
226
|
+
* Strategy: Track exact set of used nonces per sender + message type
|
|
227
|
+
* - Accept nonces that haven't been seen before
|
|
228
|
+
* - Reject duplicate nonces (replay attack)
|
|
229
|
+
* - Allows non-sequential nonces (nonce gaps are OK)
|
|
230
|
+
*
|
|
231
|
+
* Trade-off:
|
|
232
|
+
* - Higher memory usage (stores every nonce)
|
|
233
|
+
* - More flexible (allows out-of-order delivery)
|
|
234
|
+
* - Requires periodic cleanup to prevent unbounded growth
|
|
235
|
+
*/
|
|
236
|
+
export class SetBasedReceivedNonceTracker implements IReceivedNonceTracker {
|
|
237
|
+
// Map: sender -> messageType -> Set of used nonces
|
|
238
|
+
private usedNonces: Map<string, Map<string, Set<string>>> = new Map();
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Validate and record a received nonce
|
|
242
|
+
*/
|
|
243
|
+
validateAndRecord(sender: string, messageType: string, nonce: string): NonceValidationResult {
|
|
244
|
+
// Validate nonce format
|
|
245
|
+
if (!/^0x[a-fA-F0-9]{64}$/.test(nonce)) {
|
|
246
|
+
return {
|
|
247
|
+
valid: false,
|
|
248
|
+
reason: 'Invalid nonce format (must be bytes32)',
|
|
249
|
+
receivedNonce: nonce
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Get sender's nonce map
|
|
254
|
+
let senderNonces = this.usedNonces.get(sender);
|
|
255
|
+
if (!senderNonces) {
|
|
256
|
+
senderNonces = new Map<string, Set<string>>();
|
|
257
|
+
this.usedNonces.set(sender, senderNonces);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Get set of used nonces for this message type
|
|
261
|
+
let usedSet = senderNonces.get(messageType);
|
|
262
|
+
if (!usedSet) {
|
|
263
|
+
usedSet = new Set<string>();
|
|
264
|
+
senderNonces.set(messageType, usedSet);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Check if nonce was already used
|
|
268
|
+
if (usedSet.has(nonce)) {
|
|
269
|
+
return {
|
|
270
|
+
valid: false,
|
|
271
|
+
reason: 'Nonce replay detected: this nonce has already been used',
|
|
272
|
+
receivedNonce: nonce
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Valid nonce - record it
|
|
277
|
+
usedSet.add(nonce);
|
|
278
|
+
return { valid: true };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Check if a nonce has been used
|
|
283
|
+
*/
|
|
284
|
+
hasBeenUsed(sender: string, messageType: string, nonce: string): boolean {
|
|
285
|
+
const senderNonces = this.usedNonces.get(sender);
|
|
286
|
+
if (!senderNonces) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const usedSet = senderNonces.get(messageType);
|
|
291
|
+
if (!usedSet) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return usedSet.has(nonce);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Get highest nonce seen (for this strategy, compute from set)
|
|
300
|
+
*/
|
|
301
|
+
getHighestNonce(sender: string, messageType: string): string | null {
|
|
302
|
+
const senderNonces = this.usedNonces.get(sender);
|
|
303
|
+
if (!senderNonces) {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const usedSet = senderNonces.get(messageType);
|
|
308
|
+
if (!usedSet || usedSet.size === 0) {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Find maximum nonce in set
|
|
313
|
+
let maxNonce = BigInt(0);
|
|
314
|
+
usedSet.forEach(nonce => {
|
|
315
|
+
const value = BigInt(nonce);
|
|
316
|
+
if (value > maxNonce) {
|
|
317
|
+
maxNonce = value;
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
return '0x' + maxNonce.toString(16).padStart(64, '0');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Reset tracking for sender + message type
|
|
326
|
+
*/
|
|
327
|
+
reset(sender: string, messageType: string): void {
|
|
328
|
+
const senderNonces = this.usedNonces.get(sender);
|
|
329
|
+
if (senderNonces) {
|
|
330
|
+
senderNonces.delete(messageType);
|
|
331
|
+
if (senderNonces.size === 0) {
|
|
332
|
+
this.usedNonces.delete(sender);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Clear all tracked nonces
|
|
339
|
+
*/
|
|
340
|
+
clearAll(): void {
|
|
341
|
+
this.usedNonces.clear();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Get nonce count for sender + message type (for monitoring)
|
|
346
|
+
*/
|
|
347
|
+
getNonceCount(sender: string, messageType: string): number {
|
|
348
|
+
const senderNonces = this.usedNonces.get(sender);
|
|
349
|
+
if (!senderNonces) {
|
|
350
|
+
return 0;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const usedSet = senderNonces.get(messageType);
|
|
354
|
+
return usedSet ? usedSet.size : 0;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Cleanup old nonces (keep only last N)
|
|
359
|
+
* This prevents unbounded memory growth
|
|
360
|
+
*/
|
|
361
|
+
cleanup(sender: string, messageType: string, keepLast: number = 1000): void {
|
|
362
|
+
const senderNonces = this.usedNonces.get(sender);
|
|
363
|
+
if (!senderNonces) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const usedSet = senderNonces.get(messageType);
|
|
368
|
+
if (!usedSet || usedSet.size <= keepLast) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Convert to array and sort by nonce value
|
|
373
|
+
const sortedNonces = Array.from(usedSet).sort((a, b) => {
|
|
374
|
+
const aVal = BigInt(a);
|
|
375
|
+
const bVal = BigInt(b);
|
|
376
|
+
return aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Keep only the last N nonces
|
|
380
|
+
const toKeep = new Set(sortedNonces.slice(-keepLast));
|
|
381
|
+
senderNonces.set(messageType, toKeep);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Factory function to create a nonce tracker
|
|
387
|
+
* @param strategy - 'memory-efficient' (highest nonce) or 'set-based' (all nonces)
|
|
388
|
+
* @returns IReceivedNonceTracker instance
|
|
389
|
+
*/
|
|
390
|
+
export function createReceivedNonceTracker(
|
|
391
|
+
strategy: 'memory-efficient' | 'set-based' = 'memory-efficient'
|
|
392
|
+
): IReceivedNonceTracker {
|
|
393
|
+
if (strategy === 'set-based') {
|
|
394
|
+
return new SetBasedReceivedNonceTracker();
|
|
395
|
+
}
|
|
396
|
+
return new InMemoryReceivedNonceTracker();
|
|
397
|
+
}
|