@sparkleideas/shared 3.0.0-alpha.7
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 +323 -0
- package/__tests__/hooks/bash-safety.test.ts +289 -0
- package/__tests__/hooks/file-organization.test.ts +335 -0
- package/__tests__/hooks/git-commit.test.ts +336 -0
- package/__tests__/hooks/index.ts +23 -0
- package/__tests__/hooks/session-hooks.test.ts +357 -0
- package/__tests__/hooks/task-hooks.test.ts +193 -0
- package/docs/EVENTS_IMPLEMENTATION_SUMMARY.md +388 -0
- package/docs/EVENTS_QUICK_REFERENCE.md +470 -0
- package/docs/EVENTS_README.md +352 -0
- package/package.json +39 -0
- package/src/core/config/defaults.ts +207 -0
- package/src/core/config/index.ts +15 -0
- package/src/core/config/loader.ts +271 -0
- package/src/core/config/schema.ts +188 -0
- package/src/core/config/validator.ts +209 -0
- package/src/core/event-bus.ts +236 -0
- package/src/core/index.ts +22 -0
- package/src/core/interfaces/agent.interface.ts +251 -0
- package/src/core/interfaces/coordinator.interface.ts +363 -0
- package/src/core/interfaces/event.interface.ts +267 -0
- package/src/core/interfaces/index.ts +19 -0
- package/src/core/interfaces/memory.interface.ts +332 -0
- package/src/core/interfaces/task.interface.ts +223 -0
- package/src/core/orchestrator/event-coordinator.ts +122 -0
- package/src/core/orchestrator/health-monitor.ts +214 -0
- package/src/core/orchestrator/index.ts +89 -0
- package/src/core/orchestrator/lifecycle-manager.ts +263 -0
- package/src/core/orchestrator/session-manager.ts +279 -0
- package/src/core/orchestrator/task-manager.ts +317 -0
- package/src/events/domain-events.ts +584 -0
- package/src/events/event-store.test.ts +387 -0
- package/src/events/event-store.ts +588 -0
- package/src/events/example-usage.ts +293 -0
- package/src/events/index.ts +90 -0
- package/src/events/projections.ts +561 -0
- package/src/events/state-reconstructor.ts +349 -0
- package/src/events.ts +367 -0
- package/src/hooks/INTEGRATION.md +658 -0
- package/src/hooks/README.md +532 -0
- package/src/hooks/example-usage.ts +499 -0
- package/src/hooks/executor.ts +379 -0
- package/src/hooks/hooks.test.ts +421 -0
- package/src/hooks/index.ts +131 -0
- package/src/hooks/registry.ts +333 -0
- package/src/hooks/safety/bash-safety.ts +604 -0
- package/src/hooks/safety/file-organization.ts +473 -0
- package/src/hooks/safety/git-commit.ts +623 -0
- package/src/hooks/safety/index.ts +46 -0
- package/src/hooks/session-hooks.ts +559 -0
- package/src/hooks/task-hooks.ts +513 -0
- package/src/hooks/types.ts +357 -0
- package/src/hooks/verify-exports.test.ts +125 -0
- package/src/index.ts +195 -0
- package/src/mcp/connection-pool.ts +438 -0
- package/src/mcp/index.ts +183 -0
- package/src/mcp/server.ts +774 -0
- package/src/mcp/session-manager.ts +428 -0
- package/src/mcp/tool-registry.ts +566 -0
- package/src/mcp/transport/http.ts +557 -0
- package/src/mcp/transport/index.ts +294 -0
- package/src/mcp/transport/stdio.ts +324 -0
- package/src/mcp/transport/websocket.ts +484 -0
- package/src/mcp/types.ts +565 -0
- package/src/plugin-interface.ts +663 -0
- package/src/plugin-loader.ts +638 -0
- package/src/plugin-registry.ts +604 -0
- package/src/plugins/index.ts +34 -0
- package/src/plugins/official/hive-mind-plugin.ts +330 -0
- package/src/plugins/official/index.ts +24 -0
- package/src/plugins/official/maestro-plugin.ts +508 -0
- package/src/plugins/types.ts +108 -0
- package/src/resilience/bulkhead.ts +277 -0
- package/src/resilience/circuit-breaker.ts +326 -0
- package/src/resilience/index.ts +26 -0
- package/src/resilience/rate-limiter.ts +420 -0
- package/src/resilience/retry.ts +224 -0
- package/src/security/index.ts +39 -0
- package/src/security/input-validation.ts +265 -0
- package/src/security/secure-random.ts +159 -0
- package/src/services/index.ts +16 -0
- package/src/services/v3-progress.service.ts +505 -0
- package/src/types/agent.types.ts +144 -0
- package/src/types/index.ts +22 -0
- package/src/types/mcp.types.ts +300 -0
- package/src/types/memory.types.ts +263 -0
- package/src/types/swarm.types.ts +255 -0
- package/src/types/task.types.ts +205 -0
- package/src/types.ts +367 -0
- package/src/utils/secure-logger.d.ts +69 -0
- package/src/utils/secure-logger.d.ts.map +1 -0
- package/src/utils/secure-logger.js +208 -0
- package/src/utils/secure-logger.js.map +1 -0
- package/src/utils/secure-logger.ts +257 -0
- package/tmp.json +0 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate Limiter
|
|
3
|
+
*
|
|
4
|
+
* Production-ready rate limiting implementations.
|
|
5
|
+
*
|
|
6
|
+
* @module v3/shared/resilience/rate-limiter
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Rate limiter options
|
|
11
|
+
*/
|
|
12
|
+
export interface RateLimiterOptions {
|
|
13
|
+
/** Maximum requests allowed in the window */
|
|
14
|
+
maxRequests: number;
|
|
15
|
+
|
|
16
|
+
/** Time window in milliseconds */
|
|
17
|
+
windowMs: number;
|
|
18
|
+
|
|
19
|
+
/** Enable sliding window (vs fixed window) */
|
|
20
|
+
slidingWindow?: boolean;
|
|
21
|
+
|
|
22
|
+
/** Key generator for per-key limiting */
|
|
23
|
+
keyGenerator?: (context: unknown) => string;
|
|
24
|
+
|
|
25
|
+
/** Skip limiter for certain requests */
|
|
26
|
+
skip?: (context: unknown) => boolean;
|
|
27
|
+
|
|
28
|
+
/** Handler when rate limit is exceeded */
|
|
29
|
+
onRateLimited?: (key: string, remaining: number, resetAt: Date) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Rate limit result
|
|
34
|
+
*/
|
|
35
|
+
export interface RateLimitResult {
|
|
36
|
+
allowed: boolean;
|
|
37
|
+
remaining: number;
|
|
38
|
+
resetAt: Date;
|
|
39
|
+
retryAfter: number; // ms until reset
|
|
40
|
+
total: number;
|
|
41
|
+
used: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Base Rate Limiter interface
|
|
46
|
+
*/
|
|
47
|
+
export interface RateLimiter {
|
|
48
|
+
/** Check if request is allowed */
|
|
49
|
+
check(key?: string): RateLimitResult;
|
|
50
|
+
|
|
51
|
+
/** Consume a request token */
|
|
52
|
+
consume(key?: string): RateLimitResult;
|
|
53
|
+
|
|
54
|
+
/** Reset a specific key or all keys */
|
|
55
|
+
reset(key?: string): void;
|
|
56
|
+
|
|
57
|
+
/** Get current status */
|
|
58
|
+
status(key?: string): RateLimitResult;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Request entry for tracking
|
|
63
|
+
*/
|
|
64
|
+
interface RequestEntry {
|
|
65
|
+
timestamp: number;
|
|
66
|
+
key: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Sliding Window Rate Limiter
|
|
71
|
+
*
|
|
72
|
+
* Uses sliding window algorithm for smooth rate limiting.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* const limiter = new SlidingWindowRateLimiter({
|
|
76
|
+
* maxRequests: 100,
|
|
77
|
+
* windowMs: 60000, // 100 requests per minute
|
|
78
|
+
* });
|
|
79
|
+
*
|
|
80
|
+
* const result = limiter.consume('user-123');
|
|
81
|
+
* if (!result.allowed) {
|
|
82
|
+
* throw new Error(`Rate limited. Retry in ${result.retryAfter}ms`);
|
|
83
|
+
* }
|
|
84
|
+
*/
|
|
85
|
+
export class SlidingWindowRateLimiter implements RateLimiter {
|
|
86
|
+
private readonly options: RateLimiterOptions;
|
|
87
|
+
private readonly requests: Map<string, RequestEntry[]> = new Map();
|
|
88
|
+
private cleanupInterval?: ReturnType<typeof setInterval>;
|
|
89
|
+
|
|
90
|
+
constructor(options: RateLimiterOptions) {
|
|
91
|
+
this.options = {
|
|
92
|
+
slidingWindow: true,
|
|
93
|
+
...options,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Periodic cleanup of old entries
|
|
97
|
+
this.cleanupInterval = setInterval(() => {
|
|
98
|
+
this.cleanup();
|
|
99
|
+
}, this.options.windowMs);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check if a request would be allowed without consuming
|
|
104
|
+
*/
|
|
105
|
+
check(key: string = 'default'): RateLimitResult {
|
|
106
|
+
this.cleanupKey(key);
|
|
107
|
+
const entries = this.requests.get(key) || [];
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
allowed: entries.length < this.options.maxRequests,
|
|
111
|
+
remaining: Math.max(0, this.options.maxRequests - entries.length),
|
|
112
|
+
resetAt: this.getResetTime(entries),
|
|
113
|
+
retryAfter: this.getRetryAfter(entries),
|
|
114
|
+
total: this.options.maxRequests,
|
|
115
|
+
used: entries.length,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Consume a request token
|
|
121
|
+
*/
|
|
122
|
+
consume(key: string = 'default'): RateLimitResult {
|
|
123
|
+
// Clean old entries first
|
|
124
|
+
this.cleanupKey(key);
|
|
125
|
+
|
|
126
|
+
let entries = this.requests.get(key);
|
|
127
|
+
if (!entries) {
|
|
128
|
+
entries = [];
|
|
129
|
+
this.requests.set(key, entries);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Check if allowed
|
|
133
|
+
if (entries.length >= this.options.maxRequests) {
|
|
134
|
+
const result: RateLimitResult = {
|
|
135
|
+
allowed: false,
|
|
136
|
+
remaining: 0,
|
|
137
|
+
resetAt: this.getResetTime(entries),
|
|
138
|
+
retryAfter: this.getRetryAfter(entries),
|
|
139
|
+
total: this.options.maxRequests,
|
|
140
|
+
used: entries.length,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
this.options.onRateLimited?.(key, 0, result.resetAt);
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Add new entry
|
|
148
|
+
entries.push({ timestamp: Date.now(), key });
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
allowed: true,
|
|
152
|
+
remaining: this.options.maxRequests - entries.length,
|
|
153
|
+
resetAt: this.getResetTime(entries),
|
|
154
|
+
retryAfter: 0,
|
|
155
|
+
total: this.options.maxRequests,
|
|
156
|
+
used: entries.length,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Reset rate limit for a key
|
|
162
|
+
*/
|
|
163
|
+
reset(key?: string): void {
|
|
164
|
+
if (key) {
|
|
165
|
+
this.requests.delete(key);
|
|
166
|
+
} else {
|
|
167
|
+
this.requests.clear();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get current status
|
|
173
|
+
*/
|
|
174
|
+
status(key: string = 'default'): RateLimitResult {
|
|
175
|
+
return this.check(key);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Cleanup resources
|
|
180
|
+
*/
|
|
181
|
+
destroy(): void {
|
|
182
|
+
if (this.cleanupInterval) {
|
|
183
|
+
clearInterval(this.cleanupInterval);
|
|
184
|
+
this.cleanupInterval = undefined;
|
|
185
|
+
}
|
|
186
|
+
this.requests.clear();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Clean old entries for a specific key
|
|
191
|
+
*/
|
|
192
|
+
private cleanupKey(key: string): void {
|
|
193
|
+
const entries = this.requests.get(key);
|
|
194
|
+
if (!entries) return;
|
|
195
|
+
|
|
196
|
+
const cutoff = Date.now() - this.options.windowMs;
|
|
197
|
+
const filtered = entries.filter((e) => e.timestamp >= cutoff);
|
|
198
|
+
|
|
199
|
+
if (filtered.length === 0) {
|
|
200
|
+
this.requests.delete(key);
|
|
201
|
+
} else if (filtered.length !== entries.length) {
|
|
202
|
+
this.requests.set(key, filtered);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Clean all old entries
|
|
208
|
+
*/
|
|
209
|
+
private cleanup(): void {
|
|
210
|
+
for (const key of this.requests.keys()) {
|
|
211
|
+
this.cleanupKey(key);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Get reset time based on oldest entry
|
|
217
|
+
*/
|
|
218
|
+
private getResetTime(entries: RequestEntry[]): Date {
|
|
219
|
+
if (entries.length === 0) {
|
|
220
|
+
return new Date(Date.now() + this.options.windowMs);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const oldest = entries[0]!;
|
|
224
|
+
return new Date(oldest.timestamp + this.options.windowMs);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get retry after time in ms
|
|
229
|
+
*/
|
|
230
|
+
private getRetryAfter(entries: RequestEntry[]): number {
|
|
231
|
+
if (entries.length < this.options.maxRequests) {
|
|
232
|
+
return 0;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const oldest = entries[0]!;
|
|
236
|
+
const resetAt = oldest.timestamp + this.options.windowMs;
|
|
237
|
+
return Math.max(0, resetAt - Date.now());
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Token Bucket Rate Limiter
|
|
243
|
+
*
|
|
244
|
+
* Uses token bucket algorithm for burst-friendly rate limiting.
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* const limiter = new TokenBucketRateLimiter({
|
|
248
|
+
* maxRequests: 10, // bucket size
|
|
249
|
+
* windowMs: 1000, // refill interval
|
|
250
|
+
* });
|
|
251
|
+
*/
|
|
252
|
+
export class TokenBucketRateLimiter implements RateLimiter {
|
|
253
|
+
private readonly options: RateLimiterOptions;
|
|
254
|
+
private readonly buckets: Map<string, { tokens: number; lastRefill: number }> = new Map();
|
|
255
|
+
private cleanupInterval?: ReturnType<typeof setInterval>;
|
|
256
|
+
|
|
257
|
+
constructor(options: RateLimiterOptions) {
|
|
258
|
+
this.options = options;
|
|
259
|
+
|
|
260
|
+
// Periodic cleanup
|
|
261
|
+
this.cleanupInterval = setInterval(() => {
|
|
262
|
+
this.cleanup();
|
|
263
|
+
}, this.options.windowMs * 10);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Check if a request would be allowed
|
|
268
|
+
*/
|
|
269
|
+
check(key: string = 'default'): RateLimitResult {
|
|
270
|
+
this.refill(key);
|
|
271
|
+
const bucket = this.getBucket(key);
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
allowed: bucket.tokens >= 1,
|
|
275
|
+
remaining: Math.floor(bucket.tokens),
|
|
276
|
+
resetAt: new Date(bucket.lastRefill + this.options.windowMs),
|
|
277
|
+
retryAfter: bucket.tokens >= 1 ? 0 : this.options.windowMs,
|
|
278
|
+
total: this.options.maxRequests,
|
|
279
|
+
used: this.options.maxRequests - Math.floor(bucket.tokens),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Consume a token
|
|
285
|
+
*/
|
|
286
|
+
consume(key: string = 'default'): RateLimitResult {
|
|
287
|
+
this.refill(key);
|
|
288
|
+
const bucket = this.getBucket(key);
|
|
289
|
+
|
|
290
|
+
if (bucket.tokens < 1) {
|
|
291
|
+
const result: RateLimitResult = {
|
|
292
|
+
allowed: false,
|
|
293
|
+
remaining: 0,
|
|
294
|
+
resetAt: new Date(bucket.lastRefill + this.options.windowMs),
|
|
295
|
+
retryAfter: this.options.windowMs,
|
|
296
|
+
total: this.options.maxRequests,
|
|
297
|
+
used: this.options.maxRequests,
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
this.options.onRateLimited?.(key, 0, result.resetAt);
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
bucket.tokens -= 1;
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
allowed: true,
|
|
308
|
+
remaining: Math.floor(bucket.tokens),
|
|
309
|
+
resetAt: new Date(bucket.lastRefill + this.options.windowMs),
|
|
310
|
+
retryAfter: 0,
|
|
311
|
+
total: this.options.maxRequests,
|
|
312
|
+
used: this.options.maxRequests - Math.floor(bucket.tokens),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Reset bucket for a key
|
|
318
|
+
*/
|
|
319
|
+
reset(key?: string): void {
|
|
320
|
+
if (key) {
|
|
321
|
+
this.buckets.delete(key);
|
|
322
|
+
} else {
|
|
323
|
+
this.buckets.clear();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Get current status
|
|
329
|
+
*/
|
|
330
|
+
status(key: string = 'default'): RateLimitResult {
|
|
331
|
+
return this.check(key);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Cleanup resources
|
|
336
|
+
*/
|
|
337
|
+
destroy(): void {
|
|
338
|
+
if (this.cleanupInterval) {
|
|
339
|
+
clearInterval(this.cleanupInterval);
|
|
340
|
+
this.cleanupInterval = undefined;
|
|
341
|
+
}
|
|
342
|
+
this.buckets.clear();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Get or create bucket for key
|
|
347
|
+
*/
|
|
348
|
+
private getBucket(key: string): { tokens: number; lastRefill: number } {
|
|
349
|
+
let bucket = this.buckets.get(key);
|
|
350
|
+
if (!bucket) {
|
|
351
|
+
bucket = { tokens: this.options.maxRequests, lastRefill: Date.now() };
|
|
352
|
+
this.buckets.set(key, bucket);
|
|
353
|
+
}
|
|
354
|
+
return bucket;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Refill tokens based on elapsed time
|
|
359
|
+
*/
|
|
360
|
+
private refill(key: string): void {
|
|
361
|
+
const bucket = this.getBucket(key);
|
|
362
|
+
const now = Date.now();
|
|
363
|
+
const elapsed = now - bucket.lastRefill;
|
|
364
|
+
|
|
365
|
+
if (elapsed >= this.options.windowMs) {
|
|
366
|
+
// Full refill after window
|
|
367
|
+
const intervals = Math.floor(elapsed / this.options.windowMs);
|
|
368
|
+
bucket.tokens = Math.min(
|
|
369
|
+
this.options.maxRequests,
|
|
370
|
+
bucket.tokens + intervals * this.options.maxRequests
|
|
371
|
+
);
|
|
372
|
+
bucket.lastRefill = now;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Clean inactive buckets
|
|
378
|
+
*/
|
|
379
|
+
private cleanup(): void {
|
|
380
|
+
const cutoff = Date.now() - this.options.windowMs * 10;
|
|
381
|
+
|
|
382
|
+
for (const [key, bucket] of this.buckets) {
|
|
383
|
+
if (bucket.lastRefill < cutoff && bucket.tokens >= this.options.maxRequests) {
|
|
384
|
+
this.buckets.delete(key);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Rate limiter middleware for Express-like frameworks
|
|
392
|
+
*/
|
|
393
|
+
export function createRateLimiterMiddleware(limiter: RateLimiter) {
|
|
394
|
+
return (req: { ip?: string; headers?: Record<string, string> }, res: {
|
|
395
|
+
status: (code: number) => { json: (body: unknown) => void };
|
|
396
|
+
setHeader: (name: string, value: string) => void;
|
|
397
|
+
}, next: () => void): void => {
|
|
398
|
+
// Get key from IP or header
|
|
399
|
+
const key = req.ip || req.headers?.['x-forwarded-for'] || 'anonymous';
|
|
400
|
+
|
|
401
|
+
const result = limiter.consume(key);
|
|
402
|
+
|
|
403
|
+
// Set rate limit headers
|
|
404
|
+
res.setHeader('X-RateLimit-Limit', String(result.total));
|
|
405
|
+
res.setHeader('X-RateLimit-Remaining', String(result.remaining));
|
|
406
|
+
res.setHeader('X-RateLimit-Reset', String(Math.ceil(result.resetAt.getTime() / 1000)));
|
|
407
|
+
|
|
408
|
+
if (!result.allowed) {
|
|
409
|
+
res.setHeader('Retry-After', String(Math.ceil(result.retryAfter / 1000)));
|
|
410
|
+
res.status(429).json({
|
|
411
|
+
error: 'Too Many Requests',
|
|
412
|
+
retryAfter: result.retryAfter,
|
|
413
|
+
resetAt: result.resetAt.toISOString(),
|
|
414
|
+
});
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
next();
|
|
419
|
+
};
|
|
420
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry with Exponential Backoff
|
|
3
|
+
*
|
|
4
|
+
* Production-ready retry logic with jitter, max retries, and error filtering.
|
|
5
|
+
*
|
|
6
|
+
* @module v3/shared/resilience/retry
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Retry options
|
|
11
|
+
*/
|
|
12
|
+
export interface RetryOptions {
|
|
13
|
+
/** Maximum number of retry attempts (default: 3) */
|
|
14
|
+
maxAttempts: number;
|
|
15
|
+
|
|
16
|
+
/** Initial delay in milliseconds (default: 100) */
|
|
17
|
+
initialDelay: number;
|
|
18
|
+
|
|
19
|
+
/** Maximum delay in milliseconds (default: 10000) */
|
|
20
|
+
maxDelay: number;
|
|
21
|
+
|
|
22
|
+
/** Backoff multiplier (default: 2) */
|
|
23
|
+
backoffMultiplier: number;
|
|
24
|
+
|
|
25
|
+
/** Jitter factor 0-1 to randomize delays (default: 0.1) */
|
|
26
|
+
jitter: number;
|
|
27
|
+
|
|
28
|
+
/** Timeout for each attempt in milliseconds (default: 30000) */
|
|
29
|
+
timeout: number;
|
|
30
|
+
|
|
31
|
+
/** Errors that should trigger a retry (default: all errors) */
|
|
32
|
+
retryableErrors?: (error: Error) => boolean;
|
|
33
|
+
|
|
34
|
+
/** Callback for each retry attempt */
|
|
35
|
+
onRetry?: (error: Error, attempt: number, delay: number) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Retry result
|
|
40
|
+
*/
|
|
41
|
+
export interface RetryResult<T> {
|
|
42
|
+
success: boolean;
|
|
43
|
+
result?: T;
|
|
44
|
+
attempts: number;
|
|
45
|
+
totalTime: number;
|
|
46
|
+
errors: Error[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Retry error with attempt history
|
|
51
|
+
*/
|
|
52
|
+
export class RetryError extends Error {
|
|
53
|
+
constructor(
|
|
54
|
+
message: string,
|
|
55
|
+
public readonly attempts: number,
|
|
56
|
+
public readonly errors: Error[],
|
|
57
|
+
public readonly totalTime: number
|
|
58
|
+
) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.name = 'RetryError';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Default retry options
|
|
66
|
+
*/
|
|
67
|
+
const DEFAULT_OPTIONS: RetryOptions = {
|
|
68
|
+
maxAttempts: 3,
|
|
69
|
+
initialDelay: 100,
|
|
70
|
+
maxDelay: 10000,
|
|
71
|
+
backoffMultiplier: 2,
|
|
72
|
+
jitter: 0.1,
|
|
73
|
+
timeout: 30000,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Retry a function with exponential backoff
|
|
78
|
+
*
|
|
79
|
+
* @param fn Function to retry
|
|
80
|
+
* @param options Retry configuration
|
|
81
|
+
* @returns Result with success/failure and metadata
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* const result = await retry(
|
|
85
|
+
* () => fetchData(),
|
|
86
|
+
* { maxAttempts: 5, initialDelay: 200 }
|
|
87
|
+
* );
|
|
88
|
+
*
|
|
89
|
+
* if (result.success) {
|
|
90
|
+
* console.log('Data:', result.result);
|
|
91
|
+
* } else {
|
|
92
|
+
* console.log('Failed after', result.attempts, 'attempts');
|
|
93
|
+
* }
|
|
94
|
+
*/
|
|
95
|
+
export async function retry<T>(
|
|
96
|
+
fn: () => Promise<T>,
|
|
97
|
+
options: Partial<RetryOptions> = {}
|
|
98
|
+
): Promise<RetryResult<T>> {
|
|
99
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
100
|
+
const errors: Error[] = [];
|
|
101
|
+
const startTime = Date.now();
|
|
102
|
+
|
|
103
|
+
for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) {
|
|
104
|
+
try {
|
|
105
|
+
// Execute with timeout
|
|
106
|
+
const result = await withTimeout(fn(), opts.timeout, attempt);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
success: true,
|
|
110
|
+
result,
|
|
111
|
+
attempts: attempt,
|
|
112
|
+
totalTime: Date.now() - startTime,
|
|
113
|
+
errors,
|
|
114
|
+
};
|
|
115
|
+
} catch (error) {
|
|
116
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
117
|
+
errors.push(err);
|
|
118
|
+
|
|
119
|
+
// Check if error is retryable
|
|
120
|
+
if (opts.retryableErrors && !opts.retryableErrors(err)) {
|
|
121
|
+
return {
|
|
122
|
+
success: false,
|
|
123
|
+
attempts: attempt,
|
|
124
|
+
totalTime: Date.now() - startTime,
|
|
125
|
+
errors,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// If this was the last attempt, don't delay
|
|
130
|
+
if (attempt >= opts.maxAttempts) {
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Calculate delay with exponential backoff and jitter
|
|
135
|
+
const baseDelay = opts.initialDelay * Math.pow(opts.backoffMultiplier, attempt - 1);
|
|
136
|
+
const jitter = baseDelay * opts.jitter * (Math.random() * 2 - 1);
|
|
137
|
+
const delay = Math.min(baseDelay + jitter, opts.maxDelay);
|
|
138
|
+
|
|
139
|
+
// Callback before retry
|
|
140
|
+
if (opts.onRetry) {
|
|
141
|
+
opts.onRetry(err, attempt, delay);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Wait before next attempt
|
|
145
|
+
await sleep(delay);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
success: false,
|
|
151
|
+
attempts: opts.maxAttempts,
|
|
152
|
+
totalTime: Date.now() - startTime,
|
|
153
|
+
errors,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Wrap a function with retry behavior
|
|
159
|
+
*
|
|
160
|
+
* @param fn Function to wrap
|
|
161
|
+
* @param options Retry configuration
|
|
162
|
+
* @returns Wrapped function that retries on failure
|
|
163
|
+
*/
|
|
164
|
+
export function withRetry<T extends (...args: unknown[]) => Promise<unknown>>(
|
|
165
|
+
fn: T,
|
|
166
|
+
options: Partial<RetryOptions> = {}
|
|
167
|
+
): (...args: Parameters<T>) => Promise<RetryResult<Awaited<ReturnType<T>>>> {
|
|
168
|
+
return async (...args: Parameters<T>) => {
|
|
169
|
+
return retry(() => fn(...args) as Promise<Awaited<ReturnType<T>>>, options);
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Execute with timeout
|
|
175
|
+
*/
|
|
176
|
+
async function withTimeout<T>(promise: Promise<T>, timeout: number, attempt: number): Promise<T> {
|
|
177
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
178
|
+
setTimeout(() => {
|
|
179
|
+
reject(new Error(`Attempt ${attempt} timed out after ${timeout}ms`));
|
|
180
|
+
}, timeout);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
return Promise.race([promise, timeoutPromise]);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Sleep for specified milliseconds
|
|
188
|
+
*/
|
|
189
|
+
function sleep(ms: number): Promise<void> {
|
|
190
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Common retryable error predicates
|
|
195
|
+
*/
|
|
196
|
+
export const RetryableErrors = {
|
|
197
|
+
/** Network errors (ECONNRESET, ETIMEDOUT, etc.) */
|
|
198
|
+
network: (error: Error): boolean => {
|
|
199
|
+
const networkCodes = ['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED', 'ENOTFOUND', 'EAI_AGAIN'];
|
|
200
|
+
return networkCodes.some((code) => error.message.includes(code));
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
/** Rate limit errors (429) */
|
|
204
|
+
rateLimit: (error: Error): boolean => {
|
|
205
|
+
return error.message.includes('429') || error.message.toLowerCase().includes('rate limit');
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
/** Server errors (5xx) */
|
|
209
|
+
serverError: (error: Error): boolean => {
|
|
210
|
+
return /5\d\d/.test(error.message) || error.message.includes('Internal Server Error');
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
/** Transient errors (network + rate limit + 5xx) */
|
|
214
|
+
transient: (error: Error): boolean => {
|
|
215
|
+
return (
|
|
216
|
+
RetryableErrors.network(error) ||
|
|
217
|
+
RetryableErrors.rateLimit(error) ||
|
|
218
|
+
RetryableErrors.serverError(error)
|
|
219
|
+
);
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
/** All errors are retryable */
|
|
223
|
+
all: (): boolean => true,
|
|
224
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Module
|
|
3
|
+
*
|
|
4
|
+
* Shared security utilities for V3 Claude Flow.
|
|
5
|
+
*
|
|
6
|
+
* @module v3/shared/security
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Secure random generation
|
|
10
|
+
export {
|
|
11
|
+
generateSecureId,
|
|
12
|
+
generateUUID,
|
|
13
|
+
generateSecureToken,
|
|
14
|
+
generateShortId,
|
|
15
|
+
generateSessionId,
|
|
16
|
+
generateAgentId,
|
|
17
|
+
generateTaskId,
|
|
18
|
+
generateMemoryId,
|
|
19
|
+
generateEventId,
|
|
20
|
+
generateSwarmId,
|
|
21
|
+
generatePatternId,
|
|
22
|
+
generateTrajectoryId,
|
|
23
|
+
secureRandomInt,
|
|
24
|
+
secureRandomChoice,
|
|
25
|
+
secureShuffleArray,
|
|
26
|
+
} from './secure-random.js';
|
|
27
|
+
|
|
28
|
+
// Input validation
|
|
29
|
+
export {
|
|
30
|
+
validateInput,
|
|
31
|
+
sanitizeString,
|
|
32
|
+
validatePath,
|
|
33
|
+
validateCommand,
|
|
34
|
+
validateTags,
|
|
35
|
+
isValidIdentifier,
|
|
36
|
+
escapeForSql,
|
|
37
|
+
type ValidationResult,
|
|
38
|
+
type ValidationOptions,
|
|
39
|
+
} from './input-validation.js';
|