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