iri-shield 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.js ADDED
@@ -0,0 +1,672 @@
1
+ 'use strict';
2
+
3
+ const bcrypt = require('bcryptjs');
4
+ const cors = require('cors');
5
+ const crypto = require('crypto');
6
+ const helmet = require('helmet');
7
+ const jwt = require('jsonwebtoken');
8
+ const pino = require('pino');
9
+ const { randomUUID } = crypto;
10
+ const { createDashboardRouter } = require('./dashboard');
11
+ const { MemoryStorage } = require('./storage');
12
+ const { SQLiteStorage } = require('./sqlite-storage');
13
+ const { MongoStorage } = require('./mongodb-storage');
14
+ const { analyzeRequest, riskFromScore, actionFromRisk } = require('./threats');
15
+ const { redactPayload, redactRequestBody } = require('./redactor');
16
+ const { buildClientContext, detectIdentityChange, getClientIp } = require('./identity');
17
+ const { applyCustomRules } = require('./rules');
18
+ const { recordBehaviour, getBehaviourDeviation } = require('./behaviour');
19
+ const { recordSequence, detectCorrelation } = require('./correlation');
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Security mode presets
23
+ // ---------------------------------------------------------------------------
24
+
25
+ const SECURITY_MODE_PRESETS = {
26
+ low: {
27
+ rateLimit: { max: 300, windowMs: 60 * 1000 },
28
+ block: { threshold: 90, durationMs: 6 * 60 * 60 * 1000 },
29
+ anomaly: { mediumThreshold: 45, highThreshold: 75, criticalThreshold: 92 }
30
+ },
31
+ medium: {
32
+ rateLimit: { max: 120, windowMs: 60 * 1000 },
33
+ block: { threshold: 80, durationMs: 24 * 60 * 60 * 1000 },
34
+ anomaly: { mediumThreshold: 35, highThreshold: 65, criticalThreshold: 90 }
35
+ },
36
+ high: {
37
+ rateLimit: { max: 30, windowMs: 60 * 1000 },
38
+ block: { threshold: 60, durationMs: 7 * 24 * 60 * 60 * 1000 },
39
+ anomaly: { mediumThreshold: 25, highThreshold: 50, criticalThreshold: 80 }
40
+ }
41
+ };
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Default configuration
45
+ // ---------------------------------------------------------------------------
46
+
47
+ const defaultConfig = {
48
+ appName: 'iri-shield',
49
+ security: 'medium',
50
+ trustProxy: false,
51
+ failureMode: 'fail-open', // 'fail-open' | 'fail-closed'
52
+ helmet: {
53
+ enabled: true,
54
+ contentSecurityPolicy: false,
55
+ crossOriginEmbedderPolicy: false
56
+ },
57
+ cors: false,
58
+ logger: true,
59
+ requestIdHeader: 'x-iri-request-id',
60
+ testing: {
61
+ enabled: false,
62
+ allowClientOverrides: false
63
+ },
64
+ rateLimit: {
65
+ enabled: true,
66
+ windowMs: 60 * 1000,
67
+ max: 120
68
+ },
69
+ block: {
70
+ enabled: true,
71
+ threshold: 80,
72
+ durationMs: 24 * 60 * 60 * 1000
73
+ },
74
+ alert: {
75
+ enabled: true,
76
+ threshold: 35
77
+ },
78
+ anomaly: {
79
+ mediumThreshold: 35,
80
+ highThreshold: 65,
81
+ criticalThreshold: 90,
82
+ singleEndpointMax: 80,
83
+ failedAuthMax: 5,
84
+ allowedMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
85
+ sensitiveEndpoints: ['/admin', '/internal', '/debug', '/.env', '/config']
86
+ },
87
+ rules: {
88
+ sqlInjection: true,
89
+ xss: true,
90
+ pathTraversal: true,
91
+ commandInjection: true,
92
+ ssti: true,
93
+ nosqlInjection: true,
94
+ ldapInjection: true,
95
+ xxe: true,
96
+ openRedirect: true,
97
+ base64Payload: true,
98
+ headerInjection: true,
99
+ secretProbe: true,
100
+ scannerDetection: true,
101
+ headerAnomaly: true,
102
+ customRules: []
103
+ },
104
+ redaction: {
105
+ enabled: true,
106
+ mask: '[REDACTED]',
107
+ fields: [
108
+ 'password', 'token', 'accessToken', 'refreshToken',
109
+ 'authorization', 'apiKey', 'secret', 'ssn',
110
+ 'aadhaar', 'email', 'phone', 'creditCard', 'cvv'
111
+ ]
112
+ },
113
+ privacy: {
114
+ hashIp: false,
115
+ retainRawIp: true,
116
+ retentionDays: 30
117
+ },
118
+ dashboard: {
119
+ enabled: true,
120
+ path: '/iri-shield',
121
+ username: 'admin',
122
+ password: 'admin',
123
+ refreshMs: 5 * 60 * 1000
124
+ },
125
+ storage: {
126
+ mode: 'memory',
127
+ sqliteFile: './data/iri-shield.sqlite',
128
+ mongoUrl: 'mongodb://localhost:27017/iri-shield'
129
+ }
130
+ };
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // Config helpers
134
+ // ---------------------------------------------------------------------------
135
+
136
+ function mergeConfig(base, override) {
137
+ const output = Object.assign({}, base);
138
+ for (const [key, value] of Object.entries(override || {})) {
139
+ if (
140
+ value &&
141
+ typeof value === 'object' &&
142
+ !Array.isArray(value) &&
143
+ base[key] &&
144
+ typeof base[key] === 'object'
145
+ ) {
146
+ output[key] = mergeConfig(base[key], value);
147
+ } else {
148
+ output[key] = value;
149
+ }
150
+ }
151
+ return output;
152
+ }
153
+
154
+ function applySecurityMode(config, userOptions) {
155
+ const mode = config.security || 'medium';
156
+ const preset = SECURITY_MODE_PRESETS[mode] || SECURITY_MODE_PRESETS.medium;
157
+ if (!userOptions.rateLimit || !userOptions.rateLimit.max) config.rateLimit.max = preset.rateLimit.max;
158
+ if (!userOptions.rateLimit || !userOptions.rateLimit.windowMs) config.rateLimit.windowMs = preset.rateLimit.windowMs;
159
+ if (!userOptions.block || !userOptions.block.threshold) config.block.threshold = preset.block.threshold;
160
+ if (!userOptions.block || !userOptions.block.durationMs) config.block.durationMs = preset.block.durationMs;
161
+ if (!userOptions.anomaly || !userOptions.anomaly.mediumThreshold) config.anomaly.mediumThreshold = preset.anomaly.mediumThreshold;
162
+ if (!userOptions.anomaly || !userOptions.anomaly.highThreshold) config.anomaly.highThreshold = preset.anomaly.highThreshold;
163
+ if (!userOptions.anomaly || !userOptions.anomaly.criticalThreshold) config.anomaly.criticalThreshold = preset.anomaly.criticalThreshold;
164
+ return config;
165
+ }
166
+
167
+ /**
168
+ * Hash an IP address for privacy mode
169
+ */
170
+ function hashIp(ip) {
171
+ return 'sha256:' + crypto.createHash('sha256').update(ip || '').digest('hex').slice(0, 16);
172
+ }
173
+
174
+ // ---------------------------------------------------------------------------
175
+ // Main factory
176
+ // ---------------------------------------------------------------------------
177
+
178
+ function createShield(options = {}) {
179
+ let config = mergeConfig(defaultConfig, options);
180
+ config = applySecurityMode(config, options);
181
+
182
+ const storage =
183
+ options.storage && typeof options.storage.getBlock === 'function'
184
+ ? options.storage
185
+ : (options._storage || createStorage(config.storage));
186
+ const logger = options._logger || (config.logger ? pino({ name: config.appName }) : null);
187
+
188
+ // --- Base middlewares ---
189
+ const baseMiddlewares = [];
190
+ if (config.helmet) {
191
+ const helmetOptions =
192
+ typeof config.helmet === 'object'
193
+ ? Object.assign({ contentSecurityPolicy: false, crossOriginEmbedderPolicy: false }, config.helmet)
194
+ : { contentSecurityPolicy: false, crossOriginEmbedderPolicy: false };
195
+ if (helmetOptions.enabled !== false) {
196
+ delete helmetOptions.enabled;
197
+ baseMiddlewares.push(helmet(helmetOptions));
198
+ }
199
+ }
200
+ if (config.cors) {
201
+ baseMiddlewares.push(cors(typeof config.cors === 'object' ? config.cors : undefined));
202
+ }
203
+
204
+ // --- Core middleware ---
205
+ const middleware = async function iriShield(req, res, next) {
206
+ const start = process.hrtime.bigint();
207
+ const requestId = req.headers[config.requestIdHeader] || randomUUID();
208
+ res.setHeader(config.requestIdHeader, requestId);
209
+
210
+ try {
211
+ // Skip dashboard routes
212
+ const reqPath = String(req.originalUrl || req.url);
213
+ if (
214
+ config.dashboard && config.dashboard.enabled &&
215
+ config.dashboard.path &&
216
+ reqPath.startsWith(config.dashboard.path)
217
+ ) {
218
+ return next();
219
+ }
220
+
221
+ // Build client context
222
+ const client = buildClientContext(req, res, config);
223
+ req.iriShieldClient = client;
224
+ const rawIp = client.ip;
225
+
226
+ // Privacy: optionally hash IP for storage
227
+ const storageIp = config.privacy && config.privacy.hashIp ? hashIp(rawIp) : rawIp;
228
+
229
+ // Find existing client record for identity change detection
230
+ const knownClient = (storage.clients && storage.clients.get) ? storage.clients.get(client.clientId) : null;
231
+ const sameUserClients =
232
+ typeof storage.findClientsByUserId === 'function'
233
+ ? storage.findClientsByUserId(client.userId)
234
+ : [];
235
+ const identityChange = detectIdentityChange(client, knownClient || sameUserClients[0]);
236
+
237
+ // --- Check active block (always use rawIp for blocking enforcement) ---
238
+ const activeBlock = storage.getBlock(rawIp);
239
+ if (activeBlock) {
240
+ const event = buildEvent(req, {
241
+ requestId,
242
+ threat: 'blocked_ip',
243
+ riskLevel: 'critical',
244
+ riskScore: activeBlock.score || config.block.threshold,
245
+ action: 'blocked',
246
+ reason: activeBlock.reason || 'ip_block_active',
247
+ storageIp
248
+ });
249
+ storage.recordEvent(event);
250
+ storage.recordRequest(Object.assign({}, client, {
251
+ ip: storageIp,
252
+ blocked: true,
253
+ endpoint: event.endpoint,
254
+ method: req.method,
255
+ statusCode: 403,
256
+ durationMs: 0
257
+ }));
258
+ return res.status(403).json({ error: 'Request blocked by iri-shield', requestId });
259
+ }
260
+
261
+ // --- Rate limit ---
262
+ const rateDecision = storage.hitRateLimit(rawIp, config.rateLimit);
263
+ if (rateDecision.blocked) {
264
+ const event = buildEvent(req, {
265
+ requestId,
266
+ threat: 'rate_limit_exceeded',
267
+ riskLevel: 'medium',
268
+ riskScore: 55,
269
+ action: 'rate_limited',
270
+ reason: 'rate_limit_' + config.rateLimit.max + '_per_' + config.rateLimit.windowMs + 'ms',
271
+ storageIp
272
+ });
273
+ storage.recordEvent(event);
274
+ storage.recordRequest(Object.assign({}, client, {
275
+ ip: storageIp,
276
+ blocked: true,
277
+ endpoint: event.endpoint,
278
+ method: req.method,
279
+ statusCode: 429,
280
+ durationMs: 0
281
+ }));
282
+ return res.status(429).json({
283
+ error: 'Too many requests — rate limit exceeded',
284
+ requestId,
285
+ retryAfter: Math.ceil(config.rateLimit.windowMs / 1000)
286
+ });
287
+ }
288
+
289
+ // --- Threat analysis ---
290
+ const analysis = analyzeRequest(req, storage, config);
291
+
292
+ // --- Custom rule engine ---
293
+ const customResult = applyCustomRules(req, config);
294
+ if (customResult.score > 0) {
295
+ analysis.score = Math.min(100, analysis.score + customResult.score);
296
+ analysis.threats.push(...customResult.threats);
297
+ analysis.reasons.push(...customResult.reasons);
298
+ analysis.breakdown.push(...customResult.breakdown);
299
+ analysis.riskLevel = riskFromScore(analysis.score, config);
300
+ analysis.action = actionFromRisk(analysis.riskLevel);
301
+ }
302
+
303
+ // --- Identity change penalty ---
304
+ if (identityChange.score > 0) {
305
+ analysis.score = Math.min(100, analysis.score + identityChange.score);
306
+ analysis.threats.push(...identityChange.threats);
307
+ analysis.reasons.push(...identityChange.reasons);
308
+ if (identityChange.score > 0) {
309
+ analysis.breakdown.push({
310
+ rule: 'identity_drift',
311
+ label: 'Identity / device change detected',
312
+ points: identityChange.score,
313
+ category: 'anomaly',
314
+ confidence: 75
315
+ });
316
+ }
317
+ analysis.riskLevel = riskFromScore(analysis.score, config);
318
+ analysis.action = actionFromRisk(analysis.riskLevel);
319
+ }
320
+
321
+ // --- Behaviour baseline tracking ---
322
+ if (storage.behaviourStore) {
323
+ recordBehaviour(rawIp, req.originalUrl || req.url, req.method, 0, storage.behaviourStore);
324
+ const deviation = getBehaviourDeviation(rawIp, storage.behaviourStore);
325
+ if (deviation.deviationPercent >= 80) {
326
+ const devPts = Math.round(deviation.deviationPercent * 0.2);
327
+ analysis.score = Math.min(100, analysis.score + devPts);
328
+ analysis.threats.push('behaviour_deviation');
329
+ analysis.reasons.push('behaviour_deviation_' + deviation.deviationPercent + 'pct');
330
+ analysis.breakdown.push({
331
+ rule: 'behaviour_deviation',
332
+ label: 'Behaviour deviation from baseline (' + deviation.deviationPercent + '% spike)',
333
+ points: devPts,
334
+ category: 'anomaly',
335
+ confidence: 78,
336
+ meta: deviation
337
+ });
338
+ analysis.riskLevel = riskFromScore(analysis.score, config);
339
+ analysis.action = actionFromRisk(analysis.riskLevel);
340
+ }
341
+ analysis.behaviourDeviation = deviation;
342
+ }
343
+
344
+ // --- Attack sequence correlation ---
345
+ if (storage.sequenceStore) {
346
+ recordSequence(rawIp, req.originalUrl || req.url, analysis.threats, storage.sequenceStore);
347
+ const correlation = detectCorrelation(rawIp, storage.sequenceStore);
348
+ if (correlation) {
349
+ analysis.score = Math.min(100, analysis.score + correlation.riskBonus);
350
+ analysis.threats.push('correlated_attack_' + correlation.pattern);
351
+ analysis.breakdown.push({
352
+ rule: 'correlated_attack_' + correlation.pattern,
353
+ label: correlation.label,
354
+ points: correlation.riskBonus,
355
+ category: 'correlation',
356
+ confidence: correlation.confidence
357
+ });
358
+ analysis.correlatedAttack = correlation;
359
+ analysis.riskLevel = riskFromScore(analysis.score, config);
360
+ analysis.action = actionFromRisk(analysis.riskLevel);
361
+ }
362
+ }
363
+
364
+ // Redact request body fields before storing in logs
365
+ let safeBody = null;
366
+ if (config.redaction && config.redaction.enabled && req.body) {
367
+ const redacted = redactPayload(req.body, config.redaction);
368
+ safeBody = redacted.value;
369
+ }
370
+
371
+ client.riskLevel = analysis.riskLevel;
372
+ storage.recordClient(Object.assign({}, client, { ip: storageIp }));
373
+ req.iriShield = { requestId, ip: rawIp, storageIp, client, analysis };
374
+
375
+ // --- Auto block if score >= block threshold ---
376
+ if (analysis.score >= config.block.threshold && config.block.enabled) {
377
+ storage.blockIp(rawIp, {
378
+ expiresAt: Date.now() + config.block.durationMs,
379
+ reason: analysis.reasons.join(', '),
380
+ score: analysis.score
381
+ });
382
+ const event = buildEvent(req, {
383
+ requestId,
384
+ threat: analysis.threats.join(', ') || 'blocked_threat',
385
+ riskLevel: analysis.riskLevel,
386
+ riskScore: analysis.score,
387
+ action: 'blocked',
388
+ reason: analysis.reasons.join('; '),
389
+ breakdown: analysis.breakdown,
390
+ confidence: analysis.confidence || 0,
391
+ correlatedAttack: analysis.correlatedAttack || null,
392
+ storageIp
393
+ });
394
+ storage.recordEvent(event);
395
+ storage.recordRequest(Object.assign({}, client, {
396
+ ip: storageIp,
397
+ blocked: true,
398
+ endpoint: req.originalUrl || req.url,
399
+ method: req.method,
400
+ statusCode: 403,
401
+ durationMs: 0
402
+ }));
403
+ return res.status(403).json({
404
+ error: 'Request blocked — threat detected by iri-shield',
405
+ requestId,
406
+ threat: analysis.threats.join(', ')
407
+ });
408
+ }
409
+
410
+ // --- Alerts ---
411
+ const alertThreshold = (config.alert && config.alert.threshold) || config.anomaly.mediumThreshold;
412
+ if (
413
+ config.alert && config.alert.enabled &&
414
+ analysis.score >= alertThreshold &&
415
+ analysis.score < config.block.threshold
416
+ ) {
417
+ storage.recordAlert(client.clientId, {
418
+ score: analysis.score,
419
+ riskLevel: analysis.riskLevel,
420
+ ip: rawIp,
421
+ threats: analysis.threats
422
+ });
423
+ }
424
+
425
+ // --- Record security event ---
426
+ if (analysis.score >= config.anomaly.mediumThreshold) {
427
+ const event = buildEvent(req, {
428
+ requestId,
429
+ threat: analysis.threats.join(', ') || 'anomaly',
430
+ riskLevel: analysis.riskLevel,
431
+ riskScore: analysis.score,
432
+ action: analysis.action,
433
+ reason: analysis.reasons.join('; '),
434
+ breakdown: analysis.breakdown,
435
+ confidence: analysis.confidence || 0,
436
+ correlatedAttack: analysis.correlatedAttack || null,
437
+ storageIp
438
+ });
439
+ storage.recordEvent(event);
440
+ logger && logger.warn(event, 'iri-shield security event');
441
+ }
442
+
443
+ // --- Block on critical score ---
444
+ if (analysis.score >= config.anomaly.criticalThreshold) {
445
+ storage.recordRequest(Object.assign({}, client, {
446
+ ip: storageIp,
447
+ blocked: true,
448
+ endpoint: req.originalUrl || req.url,
449
+ method: req.method,
450
+ statusCode: 403,
451
+ durationMs: 0
452
+ }));
453
+ return res.status(403).json({
454
+ error: 'Request blocked — critical threat detected by iri-shield',
455
+ requestId
456
+ });
457
+ }
458
+
459
+ // --- Patch response for redaction ---
460
+ patchResponse(res, storage, config);
461
+
462
+ // --- Record on finish ---
463
+ res.on('finish', function() {
464
+ const durationMs = Number(process.hrtime.bigint() - start) / 1e6;
465
+ storage.recordRequest(Object.assign({}, client, {
466
+ ip: storageIp,
467
+ endpoint: (req.route && req.route.path) || req.originalUrl || req.url,
468
+ method: req.method,
469
+ statusCode: res.statusCode,
470
+ durationMs,
471
+ blocked: res.statusCode === 403 || res.statusCode === 429
472
+ }));
473
+ // Update behaviour store with real status code
474
+ if (storage.behaviourStore) {
475
+ recordBehaviour(rawIp, req.originalUrl || req.url, req.method, res.statusCode, storage.behaviourStore);
476
+ }
477
+ });
478
+
479
+ return next();
480
+ } catch (error) {
481
+ logger && logger.error({ err: error, requestId }, 'iri-shield middleware failure');
482
+ if (config.failureMode === 'fail-closed') {
483
+ return res.status(503).json({ error: 'Security layer unavailable — request rejected', requestId });
484
+ }
485
+ return next(error); // fail-open: let traffic through on internal errors
486
+ }
487
+ };
488
+
489
+ // Chain base middlewares then core
490
+ const runBase = function iriShieldBase(req, res, next) {
491
+ let index = 0;
492
+ const step = function(err) {
493
+ if (err || index >= baseMiddlewares.length) return err ? next(err) : middleware(req, res, next);
494
+ return baseMiddlewares[index++](req, res, step);
495
+ };
496
+ return step();
497
+ };
498
+
499
+ return {
500
+ config,
501
+ storage,
502
+ middleware: runBase,
503
+ dashboard: createDashboardRouter({ storage, config }),
504
+ getStats: function() { return storage.getStats(); },
505
+ getConfig: function() { return sanitizeConfig(config); },
506
+ updateConfig: function(patch) { return mergeInto(config, patch); },
507
+ clear: function() { return storage.clear(); }
508
+ };
509
+ }
510
+
511
+ // ---------------------------------------------------------------------------
512
+ // Storage factory
513
+ // ---------------------------------------------------------------------------
514
+
515
+ function createStorage(storageConfig) {
516
+ storageConfig = storageConfig || {};
517
+ const mode = storageConfig.mode || 'memory';
518
+ if (mode === 'sqlite') {
519
+ return new SQLiteStorage({
520
+ file: storageConfig.sqliteFile || './data/iri-shield.sqlite',
521
+ maxRequestRows: storageConfig.maxRequestRows,
522
+ maxEventRows: storageConfig.maxEventRows,
523
+ retentionDays: storageConfig.retentionDays
524
+ });
525
+ }
526
+ if (mode === 'mongodb') {
527
+ return new MongoStorage({
528
+ mongoUrl: storageConfig.mongoUrl || 'mongodb://localhost:27017/iri-shield',
529
+ dbName: storageConfig.dbName || 'iri-shield'
530
+ });
531
+ }
532
+ return new MemoryStorage();
533
+ }
534
+
535
+ // ---------------------------------------------------------------------------
536
+ // Response redaction patch
537
+ // ---------------------------------------------------------------------------
538
+
539
+ function patchResponse(res, storage, config) {
540
+ if (!config.redaction || !config.redaction.enabled || res.locals.iriShieldRedactionPatched) return;
541
+ res.locals.iriShieldRedactionPatched = true;
542
+
543
+ const originalJson = res.json.bind(res);
544
+ const originalSend = res.send.bind(res);
545
+
546
+ res.json = function(payload) {
547
+ const result = redactPayload(payload, config.redaction);
548
+ if (result.redactions > 0) storage.recordRedaction(result.redactions);
549
+ return originalJson(result.value);
550
+ };
551
+
552
+ res.send = function(payload) {
553
+ if (typeof payload !== 'string') return originalSend(payload);
554
+ const result = redactPayload(payload, config.redaction);
555
+ if (result.redactions > 0) storage.recordRedaction(result.redactions);
556
+ return originalSend(result.value);
557
+ };
558
+ }
559
+
560
+ // ---------------------------------------------------------------------------
561
+ // Auth helpers
562
+ // ---------------------------------------------------------------------------
563
+
564
+ function apiKeyAuth(validKeys, options) {
565
+ options = options || {};
566
+ const keys = new Set(Array.isArray(validKeys) ? validKeys : [validKeys].filter(Boolean));
567
+ const headerName = (options.header || 'x-api-key').toLowerCase();
568
+ return function iriShieldApiKeyAuth(req, res, next) {
569
+ const key = req.headers[headerName];
570
+ if (!key || !keys.has(key)) {
571
+ return res.status(401).json({ error: 'Invalid or missing API key' });
572
+ }
573
+ return next();
574
+ };
575
+ }
576
+
577
+ function signToken(payload, secret, options) {
578
+ options = options || {};
579
+ if (!secret) throw new Error('JWT secret is required');
580
+ return jwt.sign(payload, secret, Object.assign({ expiresIn: '1h' }, options));
581
+ }
582
+
583
+ function jwtAuth(secret, options) {
584
+ options = options || {};
585
+ if (!secret) throw new Error('JWT secret is required');
586
+ return function iriShieldJwtAuth(req, res, next) {
587
+ const header = req.headers.authorization || '';
588
+ const token = header.startsWith('Bearer ') ? header.slice(7) : null;
589
+ if (!token) return res.status(401).json({ error: 'Missing bearer token' });
590
+ try {
591
+ req.user = jwt.verify(token, secret, options.verify || {});
592
+ return next();
593
+ } catch (_) {
594
+ return res.status(401).json({ error: 'Invalid bearer token' });
595
+ }
596
+ };
597
+ }
598
+
599
+ async function hashPassword(password, rounds) {
600
+ rounds = rounds || 10;
601
+ return bcrypt.hash(password, rounds);
602
+ }
603
+
604
+ async function comparePassword(password, hash) {
605
+ return bcrypt.compare(password, hash);
606
+ }
607
+
608
+ // ---------------------------------------------------------------------------
609
+ // Internal utilities
610
+ // ---------------------------------------------------------------------------
611
+
612
+ function buildEvent(req, extra) {
613
+ const client = req.iriShieldClient || {};
614
+ return Object.assign({
615
+ id: randomUUID(),
616
+ timestamp: new Date().toISOString(),
617
+ ip: extra.storageIp || client.ip || getClientIp(req),
618
+ clientId: client.clientId || null,
619
+ userId: client.userId || null,
620
+ sessionId: client.sessionId || null,
621
+ deviceId: client.deviceId || null,
622
+ fingerprint: client.fingerprint || null,
623
+ platform: client.secChUaPlatform || null,
624
+ method: req.method,
625
+ endpoint: req.originalUrl || req.url,
626
+ userAgent: client.userAgent || req.headers['user-agent'] || '',
627
+ referer: client.referer || req.headers['referer'] || '',
628
+ acceptLanguage: client.acceptLanguage || req.headers['accept-language'] || ''
629
+ }, extra);
630
+ }
631
+
632
+ function mergeInto(target, patch) {
633
+ for (const [key, value] of Object.entries(patch || {})) {
634
+ if (
635
+ value &&
636
+ typeof value === 'object' &&
637
+ !Array.isArray(value) &&
638
+ target[key] &&
639
+ typeof target[key] === 'object'
640
+ ) {
641
+ mergeInto(target[key], value);
642
+ } else {
643
+ target[key] = value;
644
+ }
645
+ }
646
+ return sanitizeConfig(target);
647
+ }
648
+
649
+ function sanitizeConfig(config) {
650
+ const copy = JSON.parse(JSON.stringify(config));
651
+ if (copy.dashboard && copy.dashboard.password) copy.dashboard.password = '';
652
+ return copy;
653
+ }
654
+
655
+ // ---------------------------------------------------------------------------
656
+ // Exports
657
+ // ---------------------------------------------------------------------------
658
+
659
+ module.exports = {
660
+ createShield,
661
+ MemoryStorage,
662
+ SQLiteStorage,
663
+ MongoStorage,
664
+ redactPayload,
665
+ analyzeRequest,
666
+ apiKeyAuth,
667
+ signToken,
668
+ jwtAuth,
669
+ hashPassword,
670
+ comparePassword,
671
+ SECURITY_MODE_PRESETS
672
+ };