nextjs-secure 0.5.0 → 0.7.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/dist/audit.cjs ADDED
@@ -0,0 +1,1337 @@
1
+ 'use strict';
2
+
3
+ // src/middleware/audit/stores/memory.ts
4
+ var MemoryStore = class {
5
+ entries = [];
6
+ maxEntries;
7
+ ttl;
8
+ constructor(options = {}) {
9
+ this.maxEntries = options.maxEntries || 1e3;
10
+ this.ttl = options.ttl || 0;
11
+ }
12
+ async write(entry) {
13
+ this.entries.push(entry);
14
+ if (this.entries.length > this.maxEntries) {
15
+ this.entries = this.entries.slice(-this.maxEntries);
16
+ }
17
+ if (this.ttl > 0) {
18
+ this.cleanExpired();
19
+ }
20
+ }
21
+ async query(options = {}) {
22
+ let result = [...this.entries];
23
+ if (options.level) {
24
+ const levels = Array.isArray(options.level) ? options.level : [options.level];
25
+ result = result.filter((e) => levels.includes(e.level));
26
+ }
27
+ if (options.type) {
28
+ result = result.filter((e) => e.type === options.type);
29
+ }
30
+ if (options.event) {
31
+ const events = Array.isArray(options.event) ? options.event : [options.event];
32
+ result = result.filter(
33
+ (e) => e.type === "security" && events.includes(e.event)
34
+ );
35
+ }
36
+ if (options.startTime) {
37
+ result = result.filter((e) => e.timestamp >= options.startTime);
38
+ }
39
+ if (options.endTime) {
40
+ result = result.filter((e) => e.timestamp <= options.endTime);
41
+ }
42
+ if (options.ip) {
43
+ result = result.filter((e) => {
44
+ if (e.type === "request") return e.request.ip === options.ip;
45
+ if (e.type === "security") return e.source.ip === options.ip;
46
+ return false;
47
+ });
48
+ }
49
+ if (options.userId) {
50
+ result = result.filter((e) => {
51
+ if (e.type === "request") return e.user?.id === options.userId;
52
+ if (e.type === "security") return e.source.userId === options.userId;
53
+ return false;
54
+ });
55
+ }
56
+ if (options.offset) {
57
+ result = result.slice(options.offset);
58
+ }
59
+ if (options.limit) {
60
+ result = result.slice(0, options.limit);
61
+ }
62
+ return result;
63
+ }
64
+ async flush() {
65
+ }
66
+ async close() {
67
+ this.entries = [];
68
+ }
69
+ /**
70
+ * Get all entries (for testing)
71
+ */
72
+ getEntries() {
73
+ return [...this.entries];
74
+ }
75
+ /**
76
+ * Clear all entries
77
+ */
78
+ clear() {
79
+ this.entries = [];
80
+ }
81
+ /**
82
+ * Get entry count
83
+ */
84
+ size() {
85
+ return this.entries.length;
86
+ }
87
+ /**
88
+ * Clean expired entries
89
+ */
90
+ cleanExpired() {
91
+ if (this.ttl <= 0) return;
92
+ const now = Date.now();
93
+ this.entries = this.entries.filter((e) => {
94
+ const age = now - e.timestamp.getTime();
95
+ return age < this.ttl;
96
+ });
97
+ }
98
+ };
99
+ function createMemoryStore(options) {
100
+ return new MemoryStore(options);
101
+ }
102
+
103
+ // src/middleware/audit/stores/console.ts
104
+ var COLORS = {
105
+ reset: "\x1B[0m",
106
+ bold: "\x1B[1m",
107
+ dim: "\x1B[2m",
108
+ // Log levels
109
+ debug: "\x1B[36m",
110
+ // Cyan
111
+ info: "\x1B[32m",
112
+ // Green
113
+ warn: "\x1B[33m",
114
+ // Yellow
115
+ error: "\x1B[31m",
116
+ // Red
117
+ critical: "\x1B[35m",
118
+ // Magenta
119
+ // Security severity
120
+ low: "\x1B[36m",
121
+ // Cyan
122
+ medium: "\x1B[33m",
123
+ // Yellow
124
+ high: "\x1B[31m",
125
+ // Red
126
+ // Other
127
+ timestamp: "\x1B[90m",
128
+ // Gray
129
+ method: "\x1B[34m",
130
+ // Blue
131
+ status2xx: "\x1B[32m",
132
+ // Green
133
+ status3xx: "\x1B[36m",
134
+ // Cyan
135
+ status4xx: "\x1B[33m",
136
+ // Yellow
137
+ status5xx: "\x1B[31m"
138
+ // Red
139
+ };
140
+ var LEVEL_PRIORITY = {
141
+ debug: 0,
142
+ info: 1,
143
+ warn: 2,
144
+ error: 3,
145
+ critical: 4
146
+ };
147
+ var ConsoleStore = class {
148
+ colorize;
149
+ showTimestamp;
150
+ pretty;
151
+ minLevel;
152
+ constructor(options = {}) {
153
+ this.colorize = options.colorize ?? process.env.NODE_ENV !== "production";
154
+ this.showTimestamp = options.timestamp ?? true;
155
+ this.pretty = options.pretty ?? false;
156
+ this.minLevel = options.level || "info";
157
+ }
158
+ async write(entry) {
159
+ if (LEVEL_PRIORITY[entry.level] < LEVEL_PRIORITY[this.minLevel]) {
160
+ return;
161
+ }
162
+ const output = this.pretty ? this.formatPretty(entry) : this.formatCompact(entry);
163
+ switch (entry.level) {
164
+ case "debug":
165
+ console.debug(output);
166
+ break;
167
+ case "info":
168
+ console.info(output);
169
+ break;
170
+ case "warn":
171
+ console.warn(output);
172
+ break;
173
+ case "error":
174
+ case "critical":
175
+ console.error(output);
176
+ break;
177
+ default:
178
+ console.log(output);
179
+ }
180
+ }
181
+ async flush() {
182
+ }
183
+ async close() {
184
+ }
185
+ /**
186
+ * Format entry in compact single-line format
187
+ */
188
+ formatCompact(entry) {
189
+ const parts = [];
190
+ if (this.showTimestamp) {
191
+ const ts = entry.timestamp.toISOString();
192
+ parts.push(this.color(ts, "timestamp"));
193
+ }
194
+ parts.push(this.colorLevel(entry.level));
195
+ if (entry.type === "request") {
196
+ const req = entry.request;
197
+ const res = entry.response;
198
+ parts.push(this.color(req.method, "method"));
199
+ parts.push(req.path);
200
+ if (res) {
201
+ parts.push(this.colorStatus(res.status));
202
+ parts.push(this.color(`${res.duration}ms`, "dim"));
203
+ }
204
+ if (req.ip) {
205
+ parts.push(this.color(`[${req.ip}]`, "dim"));
206
+ }
207
+ if (entry.user?.id) {
208
+ parts.push(this.color(`user:${entry.user.id}`, "dim"));
209
+ }
210
+ if (entry.error) {
211
+ parts.push(this.color(`ERROR: ${entry.error.message}`, "error"));
212
+ }
213
+ } else if (entry.type === "security") {
214
+ parts.push(this.colorSeverity(entry.severity));
215
+ parts.push(entry.event);
216
+ if (entry.source.ip) {
217
+ parts.push(this.color(`[${entry.source.ip}]`, "dim"));
218
+ }
219
+ if (entry.source.userId) {
220
+ parts.push(this.color(`user:${entry.source.userId}`, "dim"));
221
+ }
222
+ parts.push(entry.message);
223
+ }
224
+ return parts.join(" ");
225
+ }
226
+ /**
227
+ * Format entry in pretty multi-line format
228
+ */
229
+ formatPretty(entry) {
230
+ const lines = [];
231
+ const header = [
232
+ this.color(entry.timestamp.toISOString(), "timestamp"),
233
+ this.colorLevel(entry.level),
234
+ `[${entry.type.toUpperCase()}]`
235
+ ].join(" ");
236
+ lines.push(header);
237
+ if (entry.type === "request") {
238
+ const req = entry.request;
239
+ const res = entry.response;
240
+ lines.push(` ${this.color(req.method, "method")} ${req.url}`);
241
+ if (req.ip) lines.push(` IP: ${req.ip}`);
242
+ if (req.userAgent) lines.push(` UA: ${req.userAgent}`);
243
+ if (res) {
244
+ lines.push(` Status: ${this.colorStatus(res.status)} (${res.duration}ms)`);
245
+ }
246
+ if (entry.user) {
247
+ lines.push(` User: ${JSON.stringify(entry.user)}`);
248
+ }
249
+ if (entry.error) {
250
+ lines.push(` ${this.color("Error:", "error")} ${entry.error.message}`);
251
+ if (entry.error.stack) {
252
+ lines.push(` ${this.color(entry.error.stack, "dim")}`);
253
+ }
254
+ }
255
+ } else if (entry.type === "security") {
256
+ lines.push(` Event: ${entry.event}`);
257
+ lines.push(` Severity: ${this.colorSeverity(entry.severity)}`);
258
+ lines.push(` Message: ${entry.message}`);
259
+ if (entry.source.ip) lines.push(` Source IP: ${entry.source.ip}`);
260
+ if (entry.source.userId) lines.push(` Source User: ${entry.source.userId}`);
261
+ if (entry.target) {
262
+ lines.push(` Target: ${JSON.stringify(entry.target)}`);
263
+ }
264
+ if (entry.details) {
265
+ lines.push(` Details: ${JSON.stringify(entry.details)}`);
266
+ }
267
+ }
268
+ if (entry.metadata && Object.keys(entry.metadata).length > 0) {
269
+ lines.push(` Metadata: ${JSON.stringify(entry.metadata)}`);
270
+ }
271
+ return lines.join("\n");
272
+ }
273
+ /**
274
+ * Apply color if enabled
275
+ */
276
+ color(text, colorName) {
277
+ if (!this.colorize) return text;
278
+ return `${COLORS[colorName]}${text}${COLORS.reset}`;
279
+ }
280
+ /**
281
+ * Color log level
282
+ */
283
+ colorLevel(level) {
284
+ const text = level.toUpperCase().padEnd(8);
285
+ if (!this.colorize) return `[${text}]`;
286
+ return `[${COLORS[level]}${text}${COLORS.reset}]`;
287
+ }
288
+ /**
289
+ * Color HTTP status
290
+ */
291
+ colorStatus(status) {
292
+ const text = status.toString();
293
+ if (!this.colorize) return text;
294
+ if (status >= 500) return `${COLORS.status5xx}${text}${COLORS.reset}`;
295
+ if (status >= 400) return `${COLORS.status4xx}${text}${COLORS.reset}`;
296
+ if (status >= 300) return `${COLORS.status3xx}${text}${COLORS.reset}`;
297
+ return `${COLORS.status2xx}${text}${COLORS.reset}`;
298
+ }
299
+ /**
300
+ * Color severity
301
+ */
302
+ colorSeverity(severity) {
303
+ const text = `[${severity.toUpperCase()}]`;
304
+ if (!this.colorize) return text;
305
+ const colorKey = severity === "critical" ? "critical" : severity;
306
+ return `${COLORS[colorKey]}${text}${COLORS.reset}`;
307
+ }
308
+ };
309
+ function createConsoleStore(options) {
310
+ return new ConsoleStore(options);
311
+ }
312
+
313
+ // src/middleware/audit/stores/external.ts
314
+ var ExternalStore = class {
315
+ endpoint;
316
+ headers;
317
+ batchSize;
318
+ flushInterval;
319
+ retryAttempts;
320
+ timeout;
321
+ buffer = [];
322
+ flushTimer = null;
323
+ isFlushing = false;
324
+ constructor(options) {
325
+ this.endpoint = options.endpoint;
326
+ this.headers = {
327
+ "Content-Type": "application/json",
328
+ ...options.apiKey ? { "Authorization": `Bearer ${options.apiKey}` } : {},
329
+ ...options.headers
330
+ };
331
+ this.batchSize = options.batchSize || 100;
332
+ this.flushInterval = options.flushInterval || 5e3;
333
+ this.retryAttempts = options.retryAttempts || 3;
334
+ this.timeout = options.timeout || 1e4;
335
+ if (this.flushInterval > 0) {
336
+ this.flushTimer = setInterval(() => this.flush(), this.flushInterval);
337
+ }
338
+ }
339
+ async write(entry) {
340
+ this.buffer.push(entry);
341
+ if (this.buffer.length >= this.batchSize) {
342
+ await this.flush();
343
+ }
344
+ }
345
+ async flush() {
346
+ if (this.isFlushing || this.buffer.length === 0) return;
347
+ this.isFlushing = true;
348
+ const entries = [...this.buffer];
349
+ this.buffer = [];
350
+ try {
351
+ await this.send(entries);
352
+ } catch (error) {
353
+ this.buffer = [...entries, ...this.buffer];
354
+ console.error("[ExternalStore] Failed to flush logs:", error);
355
+ } finally {
356
+ this.isFlushing = false;
357
+ }
358
+ }
359
+ async close() {
360
+ if (this.flushTimer) {
361
+ clearInterval(this.flushTimer);
362
+ this.flushTimer = null;
363
+ }
364
+ await this.flush();
365
+ }
366
+ /**
367
+ * Send entries to external endpoint
368
+ */
369
+ async send(entries) {
370
+ let lastError = null;
371
+ for (let attempt = 0; attempt < this.retryAttempts; attempt++) {
372
+ try {
373
+ const controller = new AbortController();
374
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
375
+ const response = await fetch(this.endpoint, {
376
+ method: "POST",
377
+ headers: this.headers,
378
+ body: JSON.stringify({
379
+ logs: entries.map((e) => this.serialize(e)),
380
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
381
+ count: entries.length
382
+ }),
383
+ signal: controller.signal
384
+ });
385
+ clearTimeout(timeoutId);
386
+ if (!response.ok) {
387
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
388
+ }
389
+ return;
390
+ } catch (error) {
391
+ lastError = error instanceof Error ? error : new Error(String(error));
392
+ if (attempt < this.retryAttempts - 1) {
393
+ await this.sleep(Math.pow(2, attempt) * 1e3);
394
+ }
395
+ }
396
+ }
397
+ throw lastError || new Error("Failed to send logs");
398
+ }
399
+ /**
400
+ * Serialize entry for transmission
401
+ */
402
+ serialize(entry) {
403
+ return {
404
+ ...entry,
405
+ timestamp: entry.timestamp.toISOString()
406
+ };
407
+ }
408
+ /**
409
+ * Sleep helper
410
+ */
411
+ sleep(ms) {
412
+ return new Promise((resolve) => setTimeout(resolve, ms));
413
+ }
414
+ /**
415
+ * Get buffer size (for monitoring)
416
+ */
417
+ getBufferSize() {
418
+ return this.buffer.length;
419
+ }
420
+ };
421
+ function createExternalStore(options) {
422
+ return new ExternalStore(options);
423
+ }
424
+ function createDatadogStore(options) {
425
+ const site = options.site || "datadoghq.com";
426
+ const endpoint = `https://http-intake.logs.${site}/api/v2/logs`;
427
+ return new ExternalStore({
428
+ endpoint,
429
+ headers: {
430
+ "DD-API-KEY": options.apiKey,
431
+ "Content-Type": "application/json"
432
+ },
433
+ batchSize: options.batchSize || 100,
434
+ flushInterval: options.flushInterval || 5e3
435
+ });
436
+ }
437
+ var MultiStore = class {
438
+ stores;
439
+ constructor(stores) {
440
+ this.stores = stores;
441
+ }
442
+ async write(entry) {
443
+ await Promise.all(this.stores.map((store) => store.write(entry)));
444
+ }
445
+ async query(options) {
446
+ for (const store of this.stores) {
447
+ if (store.query) {
448
+ return store.query(options);
449
+ }
450
+ }
451
+ return [];
452
+ }
453
+ async flush() {
454
+ await Promise.all(this.stores.map((store) => store.flush?.()));
455
+ }
456
+ async close() {
457
+ await Promise.all(this.stores.map((store) => store.close?.()));
458
+ }
459
+ };
460
+ function createMultiStore(stores) {
461
+ return new MultiStore(stores);
462
+ }
463
+
464
+ // src/middleware/audit/formatters.ts
465
+ var JSONFormatter = class {
466
+ pretty;
467
+ includeTimestamp;
468
+ constructor(options = {}) {
469
+ this.pretty = options.pretty ?? false;
470
+ this.includeTimestamp = options.includeTimestamp ?? true;
471
+ }
472
+ format(entry) {
473
+ const output = {
474
+ ...entry,
475
+ timestamp: this.includeTimestamp ? entry.timestamp.toISOString() : void 0
476
+ };
477
+ return this.pretty ? JSON.stringify(output, null, 2) : JSON.stringify(output);
478
+ }
479
+ };
480
+ var TextFormatter = class {
481
+ template;
482
+ dateFormat;
483
+ constructor(options = {}) {
484
+ this.template = options.template || "{timestamp} [{level}] {message}";
485
+ this.dateFormat = options.dateFormat || "iso";
486
+ }
487
+ format(entry) {
488
+ let output = this.template;
489
+ output = output.replace("{timestamp}", this.formatDate(entry.timestamp));
490
+ output = output.replace("{level}", entry.level.toUpperCase().padEnd(8));
491
+ output = output.replace("{message}", entry.message);
492
+ output = output.replace("{type}", entry.type);
493
+ output = output.replace("{id}", entry.id);
494
+ if (entry.category) {
495
+ output = output.replace("{category}", entry.category);
496
+ }
497
+ if (entry.type === "request") {
498
+ output = output.replace("{method}", entry.request.method);
499
+ output = output.replace("{path}", entry.request.path);
500
+ output = output.replace("{url}", entry.request.url);
501
+ output = output.replace("{ip}", entry.request.ip || "-");
502
+ output = output.replace("{status}", entry.response?.status?.toString() || "-");
503
+ output = output.replace("{duration}", entry.response?.duration?.toString() || "-");
504
+ }
505
+ if (entry.type === "security") {
506
+ output = output.replace("{event}", entry.event);
507
+ output = output.replace("{severity}", entry.severity);
508
+ }
509
+ return output;
510
+ }
511
+ formatDate(date) {
512
+ switch (this.dateFormat) {
513
+ case "utc":
514
+ return date.toUTCString();
515
+ case "local":
516
+ return date.toLocaleString();
517
+ case "iso":
518
+ default:
519
+ return date.toISOString();
520
+ }
521
+ }
522
+ };
523
+ var CLFFormatter = class {
524
+ format(entry) {
525
+ if (entry.type !== "request") {
526
+ return `[${entry.timestamp.toISOString()}] ${entry.level.toUpperCase()} ${entry.message}`;
527
+ }
528
+ const req = entry.request;
529
+ const res = entry.response;
530
+ const host = req.ip || "-";
531
+ const ident = "-";
532
+ const authuser = entry.user?.id || "-";
533
+ const date = this.formatCLFDate(entry.timestamp);
534
+ const request = `${req.method} ${req.path} HTTP/1.1`;
535
+ const status = res?.status || 0;
536
+ const bytes = res?.contentLength || 0;
537
+ return `${host} ${ident} ${authuser} [${date}] "${request}" ${status} ${bytes}`;
538
+ }
539
+ formatCLFDate(date) {
540
+ const months = [
541
+ "Jan",
542
+ "Feb",
543
+ "Mar",
544
+ "Apr",
545
+ "May",
546
+ "Jun",
547
+ "Jul",
548
+ "Aug",
549
+ "Sep",
550
+ "Oct",
551
+ "Nov",
552
+ "Dec"
553
+ ];
554
+ const day = date.getDate().toString().padStart(2, "0");
555
+ const month = months[date.getMonth()];
556
+ const year = date.getFullYear();
557
+ const hours = date.getHours().toString().padStart(2, "0");
558
+ const minutes = date.getMinutes().toString().padStart(2, "0");
559
+ const seconds = date.getSeconds().toString().padStart(2, "0");
560
+ const offset = -date.getTimezoneOffset();
561
+ const offsetSign = offset >= 0 ? "+" : "-";
562
+ const offsetHours = Math.floor(Math.abs(offset) / 60).toString().padStart(2, "0");
563
+ const offsetMins = (Math.abs(offset) % 60).toString().padStart(2, "0");
564
+ return `${day}/${month}/${year}:${hours}:${minutes}:${seconds} ${offsetSign}${offsetHours}${offsetMins}`;
565
+ }
566
+ };
567
+ var StructuredFormatter = class {
568
+ delimiter;
569
+ kvSeparator;
570
+ constructor(options = {}) {
571
+ this.delimiter = options.delimiter || " ";
572
+ this.kvSeparator = options.kvSeparator || "=";
573
+ }
574
+ format(entry) {
575
+ const pairs = [];
576
+ pairs.push(this.pair("timestamp", entry.timestamp.toISOString()));
577
+ pairs.push(this.pair("level", entry.level));
578
+ pairs.push(this.pair("type", entry.type));
579
+ pairs.push(this.pair("id", entry.id));
580
+ pairs.push(this.pair("message", this.escape(entry.message)));
581
+ if (entry.category) {
582
+ pairs.push(this.pair("category", entry.category));
583
+ }
584
+ if (entry.type === "request") {
585
+ pairs.push(this.pair("method", entry.request.method));
586
+ pairs.push(this.pair("path", entry.request.path));
587
+ if (entry.request.ip) pairs.push(this.pair("ip", entry.request.ip));
588
+ if (entry.response) {
589
+ pairs.push(this.pair("status", entry.response.status.toString()));
590
+ pairs.push(this.pair("duration_ms", entry.response.duration.toString()));
591
+ }
592
+ if (entry.user?.id) pairs.push(this.pair("user_id", entry.user.id));
593
+ if (entry.error) {
594
+ pairs.push(this.pair("error", this.escape(entry.error.message)));
595
+ }
596
+ }
597
+ if (entry.type === "security") {
598
+ pairs.push(this.pair("event", entry.event));
599
+ pairs.push(this.pair("severity", entry.severity));
600
+ if (entry.source.ip) pairs.push(this.pair("source_ip", entry.source.ip));
601
+ if (entry.source.userId) pairs.push(this.pair("source_user", entry.source.userId));
602
+ }
603
+ return pairs.join(this.delimiter);
604
+ }
605
+ pair(key, value) {
606
+ return `${key}${this.kvSeparator}${value}`;
607
+ }
608
+ escape(value) {
609
+ if (value.includes(" ") || value.includes('"')) {
610
+ return `"${value.replace(/"/g, '\\"')}"`;
611
+ }
612
+ return value;
613
+ }
614
+ };
615
+ function createJSONFormatter(options) {
616
+ return new JSONFormatter(options);
617
+ }
618
+ function createTextFormatter(options) {
619
+ return new TextFormatter(options);
620
+ }
621
+ function createCLFFormatter() {
622
+ return new CLFFormatter();
623
+ }
624
+ function createStructuredFormatter(options) {
625
+ return new StructuredFormatter(options);
626
+ }
627
+
628
+ // src/middleware/audit/redaction.ts
629
+ var DEFAULT_PII_FIELDS = [
630
+ // Authentication
631
+ "password",
632
+ "passwd",
633
+ "secret",
634
+ "token",
635
+ "api_key",
636
+ "apiKey",
637
+ "api-key",
638
+ "access_token",
639
+ "accessToken",
640
+ "refresh_token",
641
+ "refreshToken",
642
+ "authorization",
643
+ "auth",
644
+ // Personal information
645
+ "ssn",
646
+ "social_security",
647
+ "socialSecurity",
648
+ "credit_card",
649
+ "creditCard",
650
+ "card_number",
651
+ "cardNumber",
652
+ "cvv",
653
+ "cvc",
654
+ "pin",
655
+ // Contact
656
+ "email",
657
+ "phone",
658
+ "phone_number",
659
+ "phoneNumber",
660
+ "mobile",
661
+ "address",
662
+ "street",
663
+ "zip",
664
+ "zipcode",
665
+ "postal_code",
666
+ "postalCode",
667
+ // Identity
668
+ "date_of_birth",
669
+ "dateOfBirth",
670
+ "dob",
671
+ "birth_date",
672
+ "birthDate",
673
+ "passport",
674
+ "license",
675
+ "national_id",
676
+ "nationalId"
677
+ ];
678
+ function mask(value, options = {}) {
679
+ const {
680
+ char = "*",
681
+ preserveLength = false,
682
+ showFirst = 0,
683
+ showLast = 0
684
+ } = options;
685
+ if (!value) return value;
686
+ const len = value.length;
687
+ if (preserveLength) {
688
+ const first2 = value.slice(0, showFirst);
689
+ const last2 = value.slice(-showLast || len);
690
+ const middle = char.repeat(Math.max(0, len - showFirst - showLast));
691
+ return first2 + middle + (showLast > 0 ? last2 : "");
692
+ }
693
+ const maskLen = 8;
694
+ const first = showFirst > 0 ? value.slice(0, showFirst) : "";
695
+ const last = showLast > 0 ? value.slice(-showLast) : "";
696
+ return first + char.repeat(maskLen) + last;
697
+ }
698
+ function hash(value, salt = "") {
699
+ const str = salt + value;
700
+ let hash2 = 0;
701
+ for (let i = 0; i < str.length; i++) {
702
+ const char = str.charCodeAt(i);
703
+ hash2 = (hash2 << 5) - hash2 + char;
704
+ hash2 = hash2 & hash2;
705
+ }
706
+ const hex = Math.abs(hash2).toString(16).padStart(8, "0");
707
+ return hex + hex.slice(0, 8);
708
+ }
709
+ function redactValue(value, field, config) {
710
+ if (typeof value !== "string") return value;
711
+ if (!value) return value;
712
+ const shouldRedact = config.fields.some((f) => {
713
+ const fieldLower = field.toLowerCase();
714
+ const fLower = f.toLowerCase();
715
+ return fieldLower === fLower || fieldLower.endsWith("." + fLower) || fieldLower.includes("[" + fLower + "]");
716
+ });
717
+ if (!shouldRedact) return value;
718
+ if (config.customRedactor) {
719
+ return config.customRedactor(value, field);
720
+ }
721
+ switch (config.mode) {
722
+ case "mask":
723
+ return mask(value, {
724
+ char: config.maskChar || "*",
725
+ preserveLength: config.preserveLength,
726
+ showFirst: 2,
727
+ showLast: 2
728
+ });
729
+ case "hash":
730
+ return `[HASH:${hash(value)}]`;
731
+ case "remove":
732
+ return "[REDACTED]";
733
+ default:
734
+ return "[REDACTED]";
735
+ }
736
+ }
737
+ function redactObject(obj, config, path = "") {
738
+ if (typeof obj === "string") {
739
+ return redactValue(obj, path, config);
740
+ }
741
+ if (Array.isArray(obj)) {
742
+ return obj.map((item, i) => redactObject(item, config, `${path}[${i}]`));
743
+ }
744
+ if (typeof obj === "object" && obj !== null) {
745
+ const result = {};
746
+ for (const [key, value] of Object.entries(obj)) {
747
+ const newPath = path ? `${path}.${key}` : key;
748
+ result[key] = redactObject(value, config, newPath);
749
+ }
750
+ return result;
751
+ }
752
+ return obj;
753
+ }
754
+ function createRedactor(config = {}) {
755
+ const fullConfig = {
756
+ fields: config.fields || DEFAULT_PII_FIELDS,
757
+ mode: config.mode || "mask",
758
+ maskChar: config.maskChar || "*",
759
+ preserveLength: config.preserveLength || false,
760
+ customRedactor: config.customRedactor
761
+ };
762
+ return (obj) => redactObject(obj, fullConfig);
763
+ }
764
+ function redactHeaders(headers, sensitiveHeaders = ["authorization", "cookie", "x-api-key", "x-auth-token"]) {
765
+ const result = {};
766
+ for (const [key, value] of Object.entries(headers)) {
767
+ const keyLower = key.toLowerCase();
768
+ if (sensitiveHeaders.some((h) => keyLower === h.toLowerCase())) {
769
+ result[key] = "[REDACTED]";
770
+ } else {
771
+ result[key] = value;
772
+ }
773
+ }
774
+ return result;
775
+ }
776
+ function redactQuery(query, sensitiveParams = ["token", "key", "secret", "password", "auth"]) {
777
+ const result = {};
778
+ for (const [key, value] of Object.entries(query)) {
779
+ const keyLower = key.toLowerCase();
780
+ if (sensitiveParams.some((p) => keyLower.includes(p.toLowerCase()))) {
781
+ result[key] = "[REDACTED]";
782
+ } else {
783
+ result[key] = value;
784
+ }
785
+ }
786
+ return result;
787
+ }
788
+ function redactEmail(email) {
789
+ if (!email || !email.includes("@")) return mask(email);
790
+ const [, domain] = email.split("@");
791
+ return `****@${domain}`;
792
+ }
793
+ function redactCreditCard(cardNumber) {
794
+ const cleaned = cardNumber.replace(/\D/g, "");
795
+ if (cleaned.length < 4) return mask(cardNumber);
796
+ return "**** **** **** " + cleaned.slice(-4);
797
+ }
798
+ function redactPhone(phone) {
799
+ const cleaned = phone.replace(/\D/g, "");
800
+ if (cleaned.length < 4) return mask(phone);
801
+ return mask(phone, { preserveLength: true, showLast: 4 });
802
+ }
803
+ function redactIP(ip) {
804
+ if (ip.includes(":")) {
805
+ const parts2 = ip.split(":");
806
+ return parts2[0] + ":****:****:****";
807
+ }
808
+ const parts = ip.split(".");
809
+ if (parts.length !== 4) return mask(ip);
810
+ return `${parts[0]}.${parts[1]}.*.*`;
811
+ }
812
+
813
+ // src/middleware/audit/events.ts
814
+ function generateId() {
815
+ return `evt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 9)}`;
816
+ }
817
+ function severityToLevel(severity) {
818
+ switch (severity) {
819
+ case "low":
820
+ return "info";
821
+ case "medium":
822
+ return "warn";
823
+ case "high":
824
+ return "error";
825
+ case "critical":
826
+ return "critical";
827
+ }
828
+ }
829
+ var SecurityEventTracker = class {
830
+ store;
831
+ defaultSeverity;
832
+ onEvent;
833
+ constructor(config) {
834
+ this.store = config.store;
835
+ this.defaultSeverity = config.defaultSeverity || "medium";
836
+ this.onEvent = config.onEvent;
837
+ }
838
+ /**
839
+ * Track a security event
840
+ */
841
+ async track(options) {
842
+ const severity = options.severity || this.defaultSeverity;
843
+ const entry = {
844
+ id: generateId(),
845
+ timestamp: /* @__PURE__ */ new Date(),
846
+ type: "security",
847
+ level: severityToLevel(severity),
848
+ message: options.message,
849
+ event: options.event,
850
+ severity,
851
+ source: options.source || {},
852
+ target: options.target,
853
+ details: options.details,
854
+ mitigated: options.mitigated,
855
+ metadata: options.metadata
856
+ };
857
+ await this.store.write(entry);
858
+ if (this.onEvent) {
859
+ await this.onEvent(entry);
860
+ }
861
+ return entry;
862
+ }
863
+ // Convenience methods for common events
864
+ /**
865
+ * Track failed authentication
866
+ */
867
+ async authFailed(options) {
868
+ return this.track({
869
+ event: "auth.failed",
870
+ message: options.reason || "Authentication failed",
871
+ severity: "medium",
872
+ source: {
873
+ ip: options.ip,
874
+ userAgent: options.userAgent
875
+ },
876
+ details: {
877
+ attemptedEmail: options.email,
878
+ reason: options.reason
879
+ },
880
+ metadata: options.metadata
881
+ });
882
+ }
883
+ /**
884
+ * Track successful login
885
+ */
886
+ async authLogin(options) {
887
+ return this.track({
888
+ event: "auth.login",
889
+ message: `User ${options.userId} logged in`,
890
+ severity: "low",
891
+ source: {
892
+ ip: options.ip,
893
+ userAgent: options.userAgent,
894
+ userId: options.userId
895
+ },
896
+ details: {
897
+ method: options.method || "credentials"
898
+ },
899
+ metadata: options.metadata
900
+ });
901
+ }
902
+ /**
903
+ * Track logout
904
+ */
905
+ async authLogout(options) {
906
+ return this.track({
907
+ event: "auth.logout",
908
+ message: `User ${options.userId} logged out`,
909
+ severity: "low",
910
+ source: {
911
+ ip: options.ip,
912
+ userId: options.userId
913
+ },
914
+ details: {
915
+ reason: options.reason || "user"
916
+ },
917
+ metadata: options.metadata
918
+ });
919
+ }
920
+ /**
921
+ * Track permission denied
922
+ */
923
+ async permissionDenied(options) {
924
+ return this.track({
925
+ event: "auth.permission_denied",
926
+ message: `Permission denied for ${options.action} on ${options.resource}`,
927
+ severity: "medium",
928
+ source: {
929
+ ip: options.ip,
930
+ userId: options.userId
931
+ },
932
+ target: {
933
+ resource: options.resource,
934
+ action: options.action
935
+ },
936
+ details: {
937
+ requiredRole: options.requiredRole
938
+ },
939
+ metadata: options.metadata
940
+ });
941
+ }
942
+ /**
943
+ * Track rate limit exceeded
944
+ */
945
+ async rateLimitExceeded(options) {
946
+ return this.track({
947
+ event: "ratelimit.exceeded",
948
+ message: `Rate limit exceeded for ${options.endpoint}`,
949
+ severity: "medium",
950
+ source: {
951
+ ip: options.ip,
952
+ userId: options.userId
953
+ },
954
+ target: {
955
+ resource: options.endpoint
956
+ },
957
+ details: {
958
+ limit: options.limit,
959
+ window: options.window
960
+ },
961
+ metadata: options.metadata
962
+ });
963
+ }
964
+ /**
965
+ * Track CSRF validation failure
966
+ */
967
+ async csrfInvalid(options) {
968
+ return this.track({
969
+ event: "csrf.invalid",
970
+ message: `CSRF validation failed for ${options.endpoint}`,
971
+ severity: "high",
972
+ source: {
973
+ ip: options.ip,
974
+ userId: options.userId
975
+ },
976
+ target: {
977
+ resource: options.endpoint
978
+ },
979
+ details: {
980
+ reason: options.reason
981
+ },
982
+ metadata: options.metadata
983
+ });
984
+ }
985
+ /**
986
+ * Track XSS detection
987
+ */
988
+ async xssDetected(options) {
989
+ return this.track({
990
+ event: "xss.detected",
991
+ message: `XSS payload detected in ${options.field}`,
992
+ severity: "high",
993
+ source: {
994
+ ip: options.ip,
995
+ userId: options.userId
996
+ },
997
+ target: {
998
+ resource: options.endpoint
999
+ },
1000
+ details: {
1001
+ field: options.field,
1002
+ payload: options.payload?.slice(0, 100)
1003
+ // Truncate
1004
+ },
1005
+ mitigated: true,
1006
+ metadata: options.metadata
1007
+ });
1008
+ }
1009
+ /**
1010
+ * Track SQL injection detection
1011
+ */
1012
+ async sqliDetected(options) {
1013
+ return this.track({
1014
+ event: "sqli.detected",
1015
+ message: `SQL injection attempt detected in ${options.field}`,
1016
+ severity: options.severity || "high",
1017
+ source: {
1018
+ ip: options.ip,
1019
+ userId: options.userId
1020
+ },
1021
+ target: {
1022
+ resource: options.endpoint
1023
+ },
1024
+ details: {
1025
+ field: options.field,
1026
+ pattern: options.pattern
1027
+ },
1028
+ mitigated: true,
1029
+ metadata: options.metadata
1030
+ });
1031
+ }
1032
+ /**
1033
+ * Track IP block
1034
+ */
1035
+ async ipBlocked(options) {
1036
+ return this.track({
1037
+ event: "ip.blocked",
1038
+ message: `IP ${options.ip} blocked: ${options.reason}`,
1039
+ severity: "high",
1040
+ source: {
1041
+ ip: options.ip
1042
+ },
1043
+ details: {
1044
+ reason: options.reason,
1045
+ duration: options.duration
1046
+ },
1047
+ metadata: options.metadata
1048
+ });
1049
+ }
1050
+ /**
1051
+ * Track suspicious activity
1052
+ */
1053
+ async suspicious(options) {
1054
+ return this.track({
1055
+ event: "ip.suspicious",
1056
+ message: options.activity,
1057
+ severity: options.severity || "medium",
1058
+ source: {
1059
+ ip: options.ip,
1060
+ userId: options.userId
1061
+ },
1062
+ details: options.details,
1063
+ metadata: options.metadata
1064
+ });
1065
+ }
1066
+ /**
1067
+ * Track custom event
1068
+ */
1069
+ async custom(options) {
1070
+ return this.track({
1071
+ event: "custom",
1072
+ ...options
1073
+ });
1074
+ }
1075
+ };
1076
+ function createSecurityTracker(config) {
1077
+ return new SecurityEventTracker(config);
1078
+ }
1079
+ async function trackSecurityEvent(store, options) {
1080
+ const tracker = new SecurityEventTracker({ store });
1081
+ return tracker.track(options);
1082
+ }
1083
+
1084
+ // src/middleware/audit/middleware.ts
1085
+ function generateRequestId() {
1086
+ return `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 9)}`;
1087
+ }
1088
+ function getClientIP(req) {
1089
+ return req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip") || req.headers.get("cf-connecting-ip") || void 0;
1090
+ }
1091
+ function headersToRecord(headers, includeHeaders) {
1092
+ if (!includeHeaders) return {};
1093
+ const result = {};
1094
+ if (includeHeaders === true) {
1095
+ headers.forEach((value, key) => {
1096
+ result[key] = value;
1097
+ });
1098
+ } else if (Array.isArray(includeHeaders)) {
1099
+ for (const key of includeHeaders) {
1100
+ const value = headers.get(key);
1101
+ if (value) result[key] = value;
1102
+ }
1103
+ }
1104
+ return result;
1105
+ }
1106
+ function parseQuery(url) {
1107
+ const result = {};
1108
+ try {
1109
+ const urlObj = new URL(url);
1110
+ urlObj.searchParams.forEach((value, key) => {
1111
+ result[key] = value;
1112
+ });
1113
+ } catch {
1114
+ }
1115
+ return result;
1116
+ }
1117
+ function statusToLevel(status) {
1118
+ if (status >= 500) return "error";
1119
+ if (status >= 400) return "warn";
1120
+ return "info";
1121
+ }
1122
+ function shouldSkip(req, status, exclude) {
1123
+ if (!exclude) return false;
1124
+ const url = new URL(req.url);
1125
+ if (exclude.paths?.length) {
1126
+ const matchesPath = exclude.paths.some((pattern) => {
1127
+ if (pattern.includes("*")) {
1128
+ const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
1129
+ return regex.test(url.pathname);
1130
+ }
1131
+ return url.pathname === pattern || url.pathname.startsWith(pattern);
1132
+ });
1133
+ if (matchesPath) return true;
1134
+ }
1135
+ if (exclude.methods?.includes(req.method)) {
1136
+ return true;
1137
+ }
1138
+ if (exclude.statusCodes?.includes(status)) {
1139
+ return true;
1140
+ }
1141
+ return false;
1142
+ }
1143
+ function withAuditLog(handler, config) {
1144
+ const {
1145
+ enabled = true,
1146
+ store,
1147
+ include = {},
1148
+ exclude,
1149
+ pii,
1150
+ getUser,
1151
+ requestIdHeader = "x-request-id",
1152
+ generateRequestId: customGenerateId,
1153
+ onError,
1154
+ skip
1155
+ } = config;
1156
+ const includeSettings = {
1157
+ ip: include.ip ?? true,
1158
+ userAgent: include.userAgent ?? true,
1159
+ headers: include.headers ?? false,
1160
+ query: include.query ?? true,
1161
+ body: include.body ?? false,
1162
+ response: include.response ?? true,
1163
+ responseBody: include.responseBody ?? false,
1164
+ duration: include.duration ?? true,
1165
+ user: include.user ?? true
1166
+ };
1167
+ const piiConfig = pii || {
1168
+ fields: DEFAULT_PII_FIELDS,
1169
+ mode: "mask"
1170
+ };
1171
+ return async (req) => {
1172
+ if (!enabled) {
1173
+ return handler(req);
1174
+ }
1175
+ if (skip && await skip(req)) {
1176
+ return handler(req);
1177
+ }
1178
+ const startTime = Date.now();
1179
+ const requestId = req.headers.get(requestIdHeader) || (customGenerateId ? customGenerateId() : generateRequestId());
1180
+ const url = new URL(req.url);
1181
+ let requestInfo = {
1182
+ id: requestId,
1183
+ method: req.method,
1184
+ url: req.url,
1185
+ path: url.pathname
1186
+ };
1187
+ if (includeSettings.ip) {
1188
+ requestInfo.ip = getClientIP(req);
1189
+ }
1190
+ if (includeSettings.userAgent) {
1191
+ requestInfo.userAgent = req.headers.get("user-agent") || void 0;
1192
+ }
1193
+ if (includeSettings.headers) {
1194
+ let headers = headersToRecord(req.headers, includeSettings.headers);
1195
+ headers = redactHeaders(headers);
1196
+ requestInfo.headers = headers;
1197
+ }
1198
+ if (includeSettings.query) {
1199
+ let query = parseQuery(req.url);
1200
+ query = redactQuery(query);
1201
+ requestInfo.query = query;
1202
+ }
1203
+ requestInfo.contentType = req.headers.get("content-type") || void 0;
1204
+ requestInfo.contentLength = parseInt(req.headers.get("content-length") || "0", 10) || void 0;
1205
+ let user;
1206
+ if (includeSettings.user && getUser) {
1207
+ try {
1208
+ user = await getUser(req) || void 0;
1209
+ } catch {
1210
+ }
1211
+ }
1212
+ let response;
1213
+ let error;
1214
+ try {
1215
+ response = await handler(req);
1216
+ } catch (err) {
1217
+ error = err instanceof Error ? err : new Error(String(err));
1218
+ throw err;
1219
+ } finally {
1220
+ const duration = Date.now() - startTime;
1221
+ const status = response?.status || 500;
1222
+ if (!shouldSkip(req, status, exclude)) {
1223
+ const entry = {
1224
+ id: requestId,
1225
+ timestamp: /* @__PURE__ */ new Date(),
1226
+ type: "request",
1227
+ level: error ? "error" : statusToLevel(status),
1228
+ message: `${req.method} ${url.pathname} ${status} ${duration}ms`,
1229
+ request: requestInfo,
1230
+ user
1231
+ };
1232
+ if (includeSettings.response && response) {
1233
+ entry.response = {
1234
+ status: response.status,
1235
+ duration
1236
+ };
1237
+ if (includeSettings.headers) {
1238
+ entry.response.headers = headersToRecord(response.headers, includeSettings.headers);
1239
+ }
1240
+ }
1241
+ if (error) {
1242
+ entry.error = {
1243
+ name: error.name,
1244
+ message: error.message,
1245
+ stack: error.stack
1246
+ };
1247
+ }
1248
+ const redactedEntry = redactObject(entry, piiConfig);
1249
+ try {
1250
+ await store.write(redactedEntry);
1251
+ } catch (writeError) {
1252
+ if (onError) {
1253
+ onError(writeError instanceof Error ? writeError : new Error(String(writeError)), entry);
1254
+ } else {
1255
+ console.error("[AuditLog] Failed to write log:", writeError);
1256
+ }
1257
+ }
1258
+ }
1259
+ }
1260
+ return response;
1261
+ };
1262
+ }
1263
+ function createAuditMiddleware(config) {
1264
+ return (handler) => withAuditLog(handler, config);
1265
+ }
1266
+ function withRequestId(handler, options = {}) {
1267
+ const { headerName = "x-request-id", generateId: generateId2 = generateRequestId } = options;
1268
+ return async (req) => {
1269
+ const requestId = req.headers.get(headerName) || generateId2();
1270
+ const response = await handler(req);
1271
+ const newResponse = new Response(response.body, {
1272
+ status: response.status,
1273
+ statusText: response.statusText,
1274
+ headers: new Headers(response.headers)
1275
+ });
1276
+ newResponse.headers.set(headerName, requestId);
1277
+ return newResponse;
1278
+ };
1279
+ }
1280
+ function withTiming(handler, options = {}) {
1281
+ const { headerName = "x-response-time", log = false } = options;
1282
+ return async (req) => {
1283
+ const start = Date.now();
1284
+ const response = await handler(req);
1285
+ const duration = Date.now() - start;
1286
+ const newResponse = new Response(response.body, {
1287
+ status: response.status,
1288
+ statusText: response.statusText,
1289
+ headers: new Headers(response.headers)
1290
+ });
1291
+ newResponse.headers.set(headerName, `${duration}ms`);
1292
+ if (log) {
1293
+ const url = new URL(req.url);
1294
+ console.log(`${req.method} ${url.pathname} ${response.status} ${duration}ms`);
1295
+ }
1296
+ return newResponse;
1297
+ };
1298
+ }
1299
+
1300
+ exports.CLFFormatter = CLFFormatter;
1301
+ exports.ConsoleStore = ConsoleStore;
1302
+ exports.DEFAULT_PII_FIELDS = DEFAULT_PII_FIELDS;
1303
+ exports.ExternalStore = ExternalStore;
1304
+ exports.JSONFormatter = JSONFormatter;
1305
+ exports.MemoryStore = MemoryStore;
1306
+ exports.MultiStore = MultiStore;
1307
+ exports.SecurityEventTracker = SecurityEventTracker;
1308
+ exports.StructuredFormatter = StructuredFormatter;
1309
+ exports.TextFormatter = TextFormatter;
1310
+ exports.createAuditMiddleware = createAuditMiddleware;
1311
+ exports.createCLFFormatter = createCLFFormatter;
1312
+ exports.createConsoleStore = createConsoleStore;
1313
+ exports.createDatadogStore = createDatadogStore;
1314
+ exports.createExternalStore = createExternalStore;
1315
+ exports.createJSONFormatter = createJSONFormatter;
1316
+ exports.createMemoryStore = createMemoryStore;
1317
+ exports.createMultiStore = createMultiStore;
1318
+ exports.createRedactor = createRedactor;
1319
+ exports.createSecurityTracker = createSecurityTracker;
1320
+ exports.createStructuredFormatter = createStructuredFormatter;
1321
+ exports.createTextFormatter = createTextFormatter;
1322
+ exports.hash = hash;
1323
+ exports.mask = mask;
1324
+ exports.redactCreditCard = redactCreditCard;
1325
+ exports.redactEmail = redactEmail;
1326
+ exports.redactHeaders = redactHeaders;
1327
+ exports.redactIP = redactIP;
1328
+ exports.redactObject = redactObject;
1329
+ exports.redactPhone = redactPhone;
1330
+ exports.redactQuery = redactQuery;
1331
+ exports.redactValue = redactValue;
1332
+ exports.trackSecurityEvent = trackSecurityEvent;
1333
+ exports.withAuditLog = withAuditLog;
1334
+ exports.withRequestId = withRequestId;
1335
+ exports.withTiming = withTiming;
1336
+ //# sourceMappingURL=audit.cjs.map
1337
+ //# sourceMappingURL=audit.cjs.map