s3db.js 13.5.1 → 13.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +25 -10
  2. package/dist/{s3db.cjs.js → s3db.cjs} +30323 -24958
  3. package/dist/s3db.cjs.map +1 -0
  4. package/dist/s3db.es.js +24026 -18654
  5. package/dist/s3db.es.js.map +1 -1
  6. package/package.json +216 -20
  7. package/src/concerns/id.js +90 -6
  8. package/src/concerns/index.js +2 -1
  9. package/src/concerns/password-hashing.js +150 -0
  10. package/src/database.class.js +4 -0
  11. package/src/plugins/api/auth/basic-auth.js +23 -1
  12. package/src/plugins/api/auth/index.js +49 -3
  13. package/src/plugins/api/auth/oauth2-auth.js +171 -0
  14. package/src/plugins/api/auth/oidc-auth.js +789 -0
  15. package/src/plugins/api/auth/oidc-client.js +462 -0
  16. package/src/plugins/api/auth/path-auth-matcher.js +284 -0
  17. package/src/plugins/api/concerns/event-emitter.js +134 -0
  18. package/src/plugins/api/concerns/failban-manager.js +651 -0
  19. package/src/plugins/api/concerns/guards-helpers.js +402 -0
  20. package/src/plugins/api/concerns/metrics-collector.js +346 -0
  21. package/src/plugins/api/index.js +503 -54
  22. package/src/plugins/api/middlewares/failban.js +305 -0
  23. package/src/plugins/api/middlewares/rate-limit.js +301 -0
  24. package/src/plugins/api/middlewares/request-id.js +74 -0
  25. package/src/plugins/api/middlewares/security-headers.js +120 -0
  26. package/src/plugins/api/middlewares/session-tracking.js +194 -0
  27. package/src/plugins/api/routes/auth-routes.js +23 -3
  28. package/src/plugins/api/routes/resource-routes.js +71 -29
  29. package/src/plugins/api/server.js +1017 -94
  30. package/src/plugins/api/utils/guards.js +213 -0
  31. package/src/plugins/api/utils/mime-types.js +154 -0
  32. package/src/plugins/api/utils/openapi-generator.js +44 -11
  33. package/src/plugins/api/utils/path-matcher.js +173 -0
  34. package/src/plugins/api/utils/static-filesystem.js +262 -0
  35. package/src/plugins/api/utils/static-s3.js +231 -0
  36. package/src/plugins/api/utils/template-engine.js +188 -0
  37. package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +853 -0
  38. package/src/plugins/cloud-inventory/drivers/aws-driver.js +2554 -0
  39. package/src/plugins/cloud-inventory/drivers/azure-driver.js +637 -0
  40. package/src/plugins/cloud-inventory/drivers/base-driver.js +99 -0
  41. package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +620 -0
  42. package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +698 -0
  43. package/src/plugins/cloud-inventory/drivers/gcp-driver.js +645 -0
  44. package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +559 -0
  45. package/src/plugins/cloud-inventory/drivers/linode-driver.js +614 -0
  46. package/src/plugins/cloud-inventory/drivers/mock-drivers.js +449 -0
  47. package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +771 -0
  48. package/src/plugins/cloud-inventory/drivers/oracle-driver.js +768 -0
  49. package/src/plugins/cloud-inventory/drivers/vultr-driver.js +636 -0
  50. package/src/plugins/cloud-inventory/index.js +20 -0
  51. package/src/plugins/cloud-inventory/registry.js +146 -0
  52. package/src/plugins/cloud-inventory/terraform-exporter.js +362 -0
  53. package/src/plugins/cloud-inventory.plugin.js +1333 -0
  54. package/src/plugins/concerns/plugin-dependencies.js +61 -1
  55. package/src/plugins/eventual-consistency/analytics.js +1 -0
  56. package/src/plugins/identity/README.md +335 -0
  57. package/src/plugins/identity/concerns/mfa-manager.js +204 -0
  58. package/src/plugins/identity/concerns/password.js +138 -0
  59. package/src/plugins/identity/concerns/resource-schemas.js +273 -0
  60. package/src/plugins/identity/concerns/token-generator.js +172 -0
  61. package/src/plugins/identity/email-service.js +422 -0
  62. package/src/plugins/identity/index.js +1052 -0
  63. package/src/plugins/identity/oauth2-server.js +1033 -0
  64. package/src/plugins/identity/oidc-discovery.js +285 -0
  65. package/src/plugins/identity/rsa-keys.js +323 -0
  66. package/src/plugins/identity/server.js +500 -0
  67. package/src/plugins/identity/session-manager.js +453 -0
  68. package/src/plugins/identity/ui/layouts/base.js +251 -0
  69. package/src/plugins/identity/ui/middleware.js +135 -0
  70. package/src/plugins/identity/ui/pages/admin/client-form.js +247 -0
  71. package/src/plugins/identity/ui/pages/admin/clients.js +179 -0
  72. package/src/plugins/identity/ui/pages/admin/dashboard.js +181 -0
  73. package/src/plugins/identity/ui/pages/admin/user-form.js +283 -0
  74. package/src/plugins/identity/ui/pages/admin/users.js +263 -0
  75. package/src/plugins/identity/ui/pages/consent.js +262 -0
  76. package/src/plugins/identity/ui/pages/forgot-password.js +104 -0
  77. package/src/plugins/identity/ui/pages/login.js +144 -0
  78. package/src/plugins/identity/ui/pages/mfa-backup-codes.js +180 -0
  79. package/src/plugins/identity/ui/pages/mfa-enrollment.js +187 -0
  80. package/src/plugins/identity/ui/pages/mfa-verification.js +178 -0
  81. package/src/plugins/identity/ui/pages/oauth-error.js +225 -0
  82. package/src/plugins/identity/ui/pages/profile.js +361 -0
  83. package/src/plugins/identity/ui/pages/register.js +226 -0
  84. package/src/plugins/identity/ui/pages/reset-password.js +128 -0
  85. package/src/plugins/identity/ui/pages/verify-email.js +172 -0
  86. package/src/plugins/identity/ui/routes.js +2541 -0
  87. package/src/plugins/identity/ui/styles/main.css +465 -0
  88. package/src/plugins/index.js +4 -1
  89. package/src/plugins/ml/base-model.class.js +32 -7
  90. package/src/plugins/ml/classification-model.class.js +1 -1
  91. package/src/plugins/ml/timeseries-model.class.js +3 -1
  92. package/src/plugins/ml.plugin.js +124 -32
  93. package/src/plugins/shared/error-handler.js +147 -0
  94. package/src/plugins/shared/index.js +9 -0
  95. package/src/plugins/shared/middlewares/compression.js +117 -0
  96. package/src/plugins/shared/middlewares/cors.js +49 -0
  97. package/src/plugins/shared/middlewares/index.js +11 -0
  98. package/src/plugins/shared/middlewares/logging.js +54 -0
  99. package/src/plugins/shared/middlewares/rate-limit.js +73 -0
  100. package/src/plugins/shared/middlewares/security.js +158 -0
  101. package/src/plugins/shared/response-formatter.js +264 -0
  102. package/src/resource.class.js +140 -12
  103. package/src/schema.class.js +30 -1
  104. package/src/validator.class.js +57 -6
  105. package/dist/s3db.cjs.js.map +0 -1
@@ -0,0 +1,651 @@
1
+ /**
2
+ * Failban Manager - Internal IP banning manager for API Plugin
3
+ *
4
+ * fail2ban-style automatic banning system integrated into API Plugin.
5
+ * NOT a standalone plugin - managed internally by ApiServer.
6
+ *
7
+ * Features:
8
+ * - Auto-ban after multiple rate limit violations
9
+ * - Persistent ban storage in S3DB
10
+ * - TTL-based auto-unban
11
+ * - IP Whitelist/Blacklist support
12
+ * - GeoIP Country blocking (MaxMind GeoLite2)
13
+ * - Events: security:banned, security:unbanned, security:violation, security:country_blocked
14
+ * - Admin endpoints for manual ban management
15
+ *
16
+ * @example
17
+ * const manager = new FailbanManager({
18
+ * database,
19
+ * enabled: true,
20
+ * maxViolations: 3,
21
+ * violationWindow: 3600000,
22
+ * banDuration: 86400000,
23
+ * whitelist: ['127.0.0.1'],
24
+ * geo: {
25
+ * enabled: true,
26
+ * databasePath: '/path/to/GeoLite2-Country.mmdb',
27
+ * allowedCountries: ['BR', 'US']
28
+ * }
29
+ * });
30
+ *
31
+ * await manager.initialize();
32
+ */
33
+
34
+ import { requirePluginDependency } from '../../concerns/plugin-dependencies.js';
35
+
36
+ export class FailbanManager {
37
+ constructor(options = {}) {
38
+ this.options = {
39
+ enabled: options.enabled !== false,
40
+ database: options.database,
41
+ maxViolations: options.maxViolations || 3,
42
+ violationWindow: options.violationWindow || 3600000,
43
+ banDuration: options.banDuration || 86400000,
44
+ whitelist: options.whitelist || ['127.0.0.1', '::1'],
45
+ blacklist: options.blacklist || [],
46
+ persistViolations: options.persistViolations !== false,
47
+ verbose: options.verbose || false,
48
+ geo: {
49
+ enabled: options.geo?.enabled || false,
50
+ databasePath: options.geo?.databasePath || null,
51
+ allowedCountries: options.geo?.allowedCountries || [],
52
+ blockedCountries: options.geo?.blockedCountries || [],
53
+ blockUnknown: options.geo?.blockUnknown || false,
54
+ cacheResults: options.geo?.cacheResults !== false
55
+ }
56
+ };
57
+
58
+ this.database = options.database;
59
+ this.bansResource = null;
60
+ this.violationsResource = null;
61
+ this.memoryCache = new Map();
62
+ this.geoCache = new Map();
63
+ this.geoReader = null;
64
+ this.cleanupTimer = null;
65
+ }
66
+
67
+ /**
68
+ * Initialize failban manager
69
+ */
70
+ async initialize() {
71
+ if (!this.options.enabled) {
72
+ if (this.options.verbose) {
73
+ console.log('[Failban] Disabled, skipping initialization');
74
+ }
75
+ return;
76
+ }
77
+
78
+ if (!this.database) {
79
+ throw new Error('[Failban] Database instance is required');
80
+ }
81
+
82
+ // Initialize GeoIP if enabled
83
+ if (this.options.geo.enabled) {
84
+ await this._initializeGeoIP();
85
+ }
86
+
87
+ // Create bans resource with TTL
88
+ this.bansResource = await this._createBansResource();
89
+
90
+ // Create violations tracking resource (optional)
91
+ if (this.options.persistViolations) {
92
+ this.violationsResource = await this._createViolationsResource();
93
+ }
94
+
95
+ // Load existing bans into memory cache
96
+ await this._loadBansIntoCache();
97
+
98
+ // Setup cleanup timer for memory cache
99
+ this._setupCleanupTimer();
100
+
101
+ if (this.options.verbose) {
102
+ console.log('[Failban] Initialized');
103
+ console.log(`[Failban] Max violations: ${this.options.maxViolations}`);
104
+ console.log(`[Failban] Violation window: ${this.options.violationWindow}ms`);
105
+ console.log(`[Failban] Ban duration: ${this.options.banDuration}ms`);
106
+ console.log(`[Failban] Whitelist: ${this.options.whitelist.join(', ')}`);
107
+
108
+ if (this.options.geo.enabled) {
109
+ console.log(`[Failban] GeoIP enabled`);
110
+ console.log(`[Failban] Allowed countries: ${this.options.geo.allowedCountries.join(', ') || 'none'}`);
111
+ console.log(`[Failban] Blocked countries: ${this.options.geo.blockedCountries.join(', ') || 'none'}`);
112
+ console.log(`[Failban] Block unknown: ${this.options.geo.blockUnknown}`);
113
+ }
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Create bans resource with TTL support
119
+ * @private
120
+ */
121
+ async _createBansResource() {
122
+ const resourceName = '_api_failban_bans';
123
+
124
+ try {
125
+ return await this.database.getResource(resourceName);
126
+ } catch (err) {
127
+ const resource = await this.database.createResource({
128
+ name: resourceName,
129
+ attributes: {
130
+ ip: 'string|required',
131
+ reason: 'string',
132
+ violations: 'number',
133
+ bannedAt: 'string',
134
+ expiresAt: 'string|required',
135
+ metadata: {
136
+ userAgent: 'string',
137
+ path: 'string',
138
+ lastViolation: 'string'
139
+ }
140
+ },
141
+ behavior: 'body-overflow',
142
+ timestamps: true,
143
+ partitions: {
144
+ byExpiry: {
145
+ fields: { expiresAtCohort: 'string' }
146
+ }
147
+ }
148
+ });
149
+
150
+ // Apply TTL plugin to this resource
151
+ const ttlPlugin = this.database.plugins?.ttl || this.database.plugins?.TTLPlugin;
152
+ if (ttlPlugin) {
153
+ ttlPlugin.options.resources = ttlPlugin.options.resources || {};
154
+ ttlPlugin.options.resources[resourceName] = {
155
+ enabled: true,
156
+ field: 'expiresAt'
157
+ };
158
+
159
+ if (this.options.verbose) {
160
+ console.log('[Failban] TTL configured for bans resource');
161
+ }
162
+ } else {
163
+ console.warn('[Failban] TTLPlugin not found - bans will not auto-expire from DB');
164
+ }
165
+
166
+ return resource;
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Create violations tracking resource
172
+ * @private
173
+ */
174
+ async _createViolationsResource() {
175
+ const resourceName = '_api_failban_violations';
176
+
177
+ try {
178
+ return await this.database.getResource(resourceName);
179
+ } catch (err) {
180
+ return await this.database.createResource({
181
+ name: resourceName,
182
+ attributes: {
183
+ ip: 'string|required',
184
+ timestamp: 'string|required',
185
+ type: 'string',
186
+ path: 'string',
187
+ userAgent: 'string'
188
+ },
189
+ behavior: 'body-overflow',
190
+ timestamps: true,
191
+ partitions: {
192
+ byIp: {
193
+ fields: { ip: 'string' }
194
+ }
195
+ }
196
+ });
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Load existing bans into memory cache
202
+ * @private
203
+ */
204
+ async _loadBansIntoCache() {
205
+ try {
206
+ const bans = await this.bansResource.list({ limit: 1000 });
207
+ const now = Date.now();
208
+
209
+ for (const ban of bans) {
210
+ const expiresAt = new Date(ban.expiresAt).getTime();
211
+ if (expiresAt > now) {
212
+ this.memoryCache.set(ban.ip, {
213
+ expiresAt,
214
+ reason: ban.reason,
215
+ violations: ban.violations
216
+ });
217
+ }
218
+ }
219
+
220
+ if (this.options.verbose) {
221
+ console.log(`[Failban] Loaded ${this.memoryCache.size} active bans into cache`);
222
+ }
223
+ } catch (err) {
224
+ console.error('[Failban] Failed to load bans:', err.message);
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Setup cleanup timer for memory cache
230
+ * @private
231
+ */
232
+ _setupCleanupTimer() {
233
+ this.cleanupTimer = setInterval(() => {
234
+ const now = Date.now();
235
+ let cleaned = 0;
236
+
237
+ for (const [ip, ban] of this.memoryCache.entries()) {
238
+ if (ban.expiresAt <= now) {
239
+ this.memoryCache.delete(ip);
240
+ cleaned++;
241
+
242
+ // Emit unban event
243
+ this.database.emit?.('security:unbanned', {
244
+ ip,
245
+ reason: 'expired',
246
+ bannedFor: ban.reason
247
+ });
248
+ }
249
+ }
250
+
251
+ if (this.options.verbose && cleaned > 0) {
252
+ console.log(`[Failban] Cleaned ${cleaned} expired bans from cache`);
253
+ }
254
+ }, 60000); // Every minute
255
+ }
256
+
257
+ /**
258
+ * Initialize GeoIP reader
259
+ * @private
260
+ */
261
+ async _initializeGeoIP() {
262
+ if (!this.options.geo.databasePath) {
263
+ console.warn('[Failban] GeoIP enabled but no databasePath provided');
264
+ return;
265
+ }
266
+
267
+ try {
268
+ const Reader = await requirePluginDependency(
269
+ '@maxmind/geoip2-node',
270
+ 'ApiPlugin (Failban)',
271
+ 'GeoIP country blocking'
272
+ );
273
+
274
+ this.geoReader = await Reader.open(this.options.geo.databasePath);
275
+
276
+ if (this.options.verbose) {
277
+ console.log(`[Failban] GeoIP database loaded from ${this.options.geo.databasePath}`);
278
+ }
279
+ } catch (err) {
280
+ console.error('[Failban] Failed to initialize GeoIP:', err.message);
281
+ console.warn('[Failban] GeoIP features will be disabled');
282
+ this.options.geo.enabled = false;
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Get country code for IP address
288
+ */
289
+ getCountryCode(ip) {
290
+ if (!this.options.geo.enabled || !this.geoReader) {
291
+ return null;
292
+ }
293
+
294
+ if (this.options.geo.cacheResults && this.geoCache.has(ip)) {
295
+ return this.geoCache.get(ip);
296
+ }
297
+
298
+ try {
299
+ const response = this.geoReader.country(ip);
300
+ const countryCode = response?.country?.isoCode || null;
301
+
302
+ if (this.options.geo.cacheResults) {
303
+ this.geoCache.set(ip, countryCode);
304
+
305
+ if (this.geoCache.size > 10000) {
306
+ const firstKey = this.geoCache.keys().next().value;
307
+ this.geoCache.delete(firstKey);
308
+ }
309
+ }
310
+
311
+ return countryCode;
312
+ } catch (err) {
313
+ if (this.options.verbose) {
314
+ console.log(`[Failban] GeoIP lookup failed for ${ip}: ${err.message}`);
315
+ }
316
+ return null;
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Check if country is blocked
322
+ */
323
+ isCountryBlocked(countryCode) {
324
+ if (!this.options.geo.enabled) {
325
+ return false;
326
+ }
327
+
328
+ if (!countryCode) {
329
+ return this.options.geo.blockUnknown;
330
+ }
331
+
332
+ const upperCode = countryCode.toUpperCase();
333
+
334
+ if (this.options.geo.blockedCountries.length > 0) {
335
+ if (this.options.geo.blockedCountries.includes(upperCode)) {
336
+ return true;
337
+ }
338
+ }
339
+
340
+ if (this.options.geo.allowedCountries.length > 0) {
341
+ return !this.options.geo.allowedCountries.includes(upperCode);
342
+ }
343
+
344
+ return false;
345
+ }
346
+
347
+ /**
348
+ * Check if IP is blocked by country restrictions
349
+ */
350
+ checkCountryBlock(ip) {
351
+ if (!this.options.geo.enabled) {
352
+ return null;
353
+ }
354
+
355
+ if (this.isWhitelisted(ip)) {
356
+ return null;
357
+ }
358
+
359
+ const countryCode = this.getCountryCode(ip);
360
+
361
+ if (this.isCountryBlocked(countryCode)) {
362
+ return {
363
+ blocked: true,
364
+ reason: 'country_restricted',
365
+ country: countryCode || 'unknown',
366
+ ip
367
+ };
368
+ }
369
+
370
+ return null;
371
+ }
372
+
373
+ /**
374
+ * Check if IP is in whitelist
375
+ */
376
+ isWhitelisted(ip) {
377
+ return this.options.whitelist.includes(ip);
378
+ }
379
+
380
+ /**
381
+ * Check if IP is in blacklist
382
+ */
383
+ isBlacklisted(ip) {
384
+ return this.options.blacklist.includes(ip);
385
+ }
386
+
387
+ /**
388
+ * Check if IP is currently banned
389
+ */
390
+ isBanned(ip) {
391
+ if (!this.options.enabled) return false;
392
+ if (this.isWhitelisted(ip)) return false;
393
+ if (this.isBlacklisted(ip)) return true;
394
+
395
+ const cachedBan = this.memoryCache.get(ip);
396
+ if (cachedBan) {
397
+ if (cachedBan.expiresAt > Date.now()) {
398
+ return true;
399
+ } else {
400
+ this.memoryCache.delete(ip);
401
+ return false;
402
+ }
403
+ }
404
+
405
+ return false;
406
+ }
407
+
408
+ /**
409
+ * Get ban details for IP
410
+ */
411
+ async getBan(ip) {
412
+ if (!this.options.enabled) return null;
413
+ if (this.isBlacklisted(ip)) {
414
+ return {
415
+ ip,
416
+ reason: 'blacklisted',
417
+ permanent: true
418
+ };
419
+ }
420
+
421
+ try {
422
+ const ban = await this.bansResource.get(ip);
423
+ if (!ban) return null;
424
+
425
+ if (new Date(ban.expiresAt).getTime() <= Date.now()) {
426
+ return null;
427
+ }
428
+
429
+ return ban;
430
+ } catch (err) {
431
+ return null;
432
+ }
433
+ }
434
+
435
+ /**
436
+ * Record a violation
437
+ */
438
+ async recordViolation(ip, type = 'rate_limit', metadata = {}) {
439
+ if (!this.options.enabled) return;
440
+ if (this.isWhitelisted(ip)) return;
441
+
442
+ const now = new Date().toISOString();
443
+
444
+ this.database.emit?.('security:violation', {
445
+ ip,
446
+ type,
447
+ timestamp: now,
448
+ ...metadata
449
+ });
450
+
451
+ if (this.violationsResource) {
452
+ try {
453
+ await this.violationsResource.insert({
454
+ id: `${ip}_${Date.now()}`,
455
+ ip,
456
+ timestamp: now,
457
+ type,
458
+ path: metadata.path,
459
+ userAgent: metadata.userAgent
460
+ });
461
+ } catch (err) {
462
+ console.error('[Failban] Failed to persist violation:', err.message);
463
+ }
464
+ }
465
+
466
+ await this._checkAndBan(ip, type, metadata);
467
+ }
468
+
469
+ /**
470
+ * Check violation count and ban if threshold exceeded
471
+ * @private
472
+ */
473
+ async _checkAndBan(ip, type, metadata) {
474
+ if (this.isBanned(ip)) return;
475
+
476
+ const cutoff = new Date(Date.now() - this.options.violationWindow).toISOString();
477
+ let violationCount = 0;
478
+
479
+ if (this.violationsResource) {
480
+ try {
481
+ const violations = await this.violationsResource.query({
482
+ ip,
483
+ timestamp: { $gte: cutoff }
484
+ });
485
+ violationCount = violations.length;
486
+ } catch (err) {
487
+ console.error('[Failban] Failed to count violations:', err.message);
488
+ return;
489
+ }
490
+ }
491
+
492
+ if (violationCount >= this.options.maxViolations) {
493
+ await this.ban(ip, `${violationCount} ${type} violations`, metadata);
494
+ }
495
+ }
496
+
497
+ /**
498
+ * Ban an IP
499
+ */
500
+ async ban(ip, reason, metadata = {}) {
501
+ if (!this.options.enabled) return;
502
+ if (this.isWhitelisted(ip)) {
503
+ console.warn(`[Failban] Cannot ban whitelisted IP: ${ip}`);
504
+ return;
505
+ }
506
+
507
+ const now = new Date();
508
+ const expiresAt = new Date(now.getTime() + this.options.banDuration);
509
+
510
+ const banRecord = {
511
+ id: ip,
512
+ ip,
513
+ reason,
514
+ violations: metadata.violationCount || this.options.maxViolations,
515
+ bannedAt: now.toISOString(),
516
+ expiresAt: expiresAt.toISOString(),
517
+ metadata: {
518
+ userAgent: metadata.userAgent,
519
+ path: metadata.path,
520
+ lastViolation: now.toISOString()
521
+ }
522
+ };
523
+
524
+ try {
525
+ await this.bansResource.insert(banRecord);
526
+
527
+ this.memoryCache.set(ip, {
528
+ expiresAt: expiresAt.getTime(),
529
+ reason,
530
+ violations: banRecord.violations
531
+ });
532
+
533
+ this.database.emit?.('security:banned', {
534
+ ip,
535
+ reason,
536
+ expiresAt: expiresAt.toISOString(),
537
+ duration: this.options.banDuration
538
+ });
539
+
540
+ if (this.options.verbose) {
541
+ console.log(`[Failban] Banned ${ip} for ${reason} until ${expiresAt.toISOString()}`);
542
+ }
543
+ } catch (err) {
544
+ console.error('[Failban] Failed to ban IP:', err.message);
545
+ }
546
+ }
547
+
548
+ /**
549
+ * Unban an IP
550
+ */
551
+ async unban(ip) {
552
+ if (!this.options.enabled) return;
553
+
554
+ try {
555
+ await this.bansResource.delete(ip);
556
+ this.memoryCache.delete(ip);
557
+
558
+ this.database.emit?.('security:unbanned', {
559
+ ip,
560
+ reason: 'manual',
561
+ unbannedAt: new Date().toISOString()
562
+ });
563
+
564
+ if (this.options.verbose) {
565
+ console.log(`[Failban] Unbanned ${ip}`);
566
+ }
567
+
568
+ return true;
569
+ } catch (err) {
570
+ console.error('[Failban] Failed to unban IP:', err.message);
571
+ return false;
572
+ }
573
+ }
574
+
575
+ /**
576
+ * List all active bans
577
+ */
578
+ async listBans() {
579
+ if (!this.options.enabled) return [];
580
+
581
+ try {
582
+ const bans = await this.bansResource.list({ limit: 1000 });
583
+ const now = Date.now();
584
+
585
+ return bans.filter(ban => new Date(ban.expiresAt).getTime() > now);
586
+ } catch (err) {
587
+ console.error('[Failban] Failed to list bans:', err.message);
588
+ return [];
589
+ }
590
+ }
591
+
592
+ /**
593
+ * Get statistics
594
+ */
595
+ async getStats() {
596
+ const activeBans = await this.listBans();
597
+
598
+ let totalViolations = 0;
599
+ if (this.violationsResource) {
600
+ try {
601
+ const violations = await this.violationsResource.list({ limit: 10000 });
602
+ totalViolations = violations.length;
603
+ } catch (err) {
604
+ console.error('[Failban] Failed to count violations:', err.message);
605
+ }
606
+ }
607
+
608
+ return {
609
+ enabled: this.options.enabled,
610
+ activeBans: activeBans.length,
611
+ cachedBans: this.memoryCache.size,
612
+ totalViolations,
613
+ whitelistedIPs: this.options.whitelist.length,
614
+ blacklistedIPs: this.options.blacklist.length,
615
+ geo: {
616
+ enabled: this.options.geo.enabled,
617
+ allowedCountries: this.options.geo.allowedCountries.length,
618
+ blockedCountries: this.options.geo.blockedCountries.length,
619
+ blockUnknown: this.options.geo.blockUnknown
620
+ },
621
+ config: {
622
+ maxViolations: this.options.maxViolations,
623
+ violationWindow: this.options.violationWindow,
624
+ banDuration: this.options.banDuration
625
+ }
626
+ };
627
+ }
628
+
629
+ /**
630
+ * Cleanup
631
+ */
632
+ async cleanup() {
633
+ if (this.cleanupTimer) {
634
+ clearInterval(this.cleanupTimer);
635
+ this.cleanupTimer = null;
636
+ }
637
+
638
+ this.memoryCache.clear();
639
+ this.geoCache.clear();
640
+
641
+ if (this.geoReader) {
642
+ this.geoReader = null;
643
+ }
644
+
645
+ if (this.options.verbose) {
646
+ console.log('[Failban] Cleaned up');
647
+ }
648
+ }
649
+ }
650
+
651
+ export default FailbanManager;