@revealui/security 0.0.1-pre.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/LICENSE +202 -0
- package/dist/index.js +1841 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1841 @@
|
|
|
1
|
+
// src/SecurityValidation.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var EnvironmentSchema = z.object({
|
|
4
|
+
PAYLOAD_SECRET: z.string().min(32, "PAYLOAD_SECRET must be at least 32 characters"),
|
|
5
|
+
MCP_ENCRYPTION_KEY: z.string().min(64, "MCP_ENCRYPTION_KEY must be at least 64 characters"),
|
|
6
|
+
TURSO_URL: z.string().url().optional(),
|
|
7
|
+
TURSO_AUTH_TOKEN: z.string().optional(),
|
|
8
|
+
SUPPORT_EMAIL: z.string().email().optional()
|
|
9
|
+
});
|
|
10
|
+
var RATE_LIMITS = {
|
|
11
|
+
CHAT: { windowMs: 60 * 1e3, maxRequests: 10 },
|
|
12
|
+
// 10 requests per minute
|
|
13
|
+
CONTENT: { windowMs: 60 * 1e3, maxRequests: 5 },
|
|
14
|
+
// 5 requests per minute
|
|
15
|
+
CONTACT: { windowMs: 15 * 60 * 1e3, maxRequests: 3 },
|
|
16
|
+
// 3 requests per 15 minutes
|
|
17
|
+
RECOMMENDATIONS: { windowMs: 60 * 1e3, maxRequests: 20 }
|
|
18
|
+
// 20 requests per minute
|
|
19
|
+
};
|
|
20
|
+
var rateLimitStore = /* @__PURE__ */ new Map();
|
|
21
|
+
async function validateEnvironment() {
|
|
22
|
+
const validation = EnvironmentSchema.safeParse(process.env);
|
|
23
|
+
if (!validation.success) {
|
|
24
|
+
const errors = validation.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join(", ");
|
|
25
|
+
throw new Error(`Environment validation failed: ${errors}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function checkRateLimit(key, config) {
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
for (const [k, v] of rateLimitStore.entries()) {
|
|
31
|
+
if (v.resetTime < now) {
|
|
32
|
+
rateLimitStore.delete(k);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const entry = rateLimitStore.get(key);
|
|
36
|
+
if (!entry || entry.resetTime < now) {
|
|
37
|
+
rateLimitStore.set(key, { count: 1, resetTime: now + config.windowMs });
|
|
38
|
+
return {
|
|
39
|
+
allowed: true,
|
|
40
|
+
remaining: config.maxRequests - 1,
|
|
41
|
+
resetTime: now + config.windowMs
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (entry.count >= config.maxRequests) {
|
|
45
|
+
return {
|
|
46
|
+
allowed: false,
|
|
47
|
+
remaining: 0,
|
|
48
|
+
resetTime: entry.resetTime
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
entry.count++;
|
|
52
|
+
rateLimitStore.set(key, entry);
|
|
53
|
+
return {
|
|
54
|
+
allowed: true,
|
|
55
|
+
remaining: config.maxRequests - entry.count,
|
|
56
|
+
resetTime: entry.resetTime
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function generateRateLimitKey(userId, action) {
|
|
60
|
+
return `rate_limit:${action}:${userId}`;
|
|
61
|
+
}
|
|
62
|
+
function generateIPRateLimitKey(ip, action) {
|
|
63
|
+
return `rate_limit:${action}:ip:${ip}`;
|
|
64
|
+
}
|
|
65
|
+
async function validateCSRFToken(token, sessionToken) {
|
|
66
|
+
return token === sessionToken && token.length > 0;
|
|
67
|
+
}
|
|
68
|
+
function sanitizeInput(input) {
|
|
69
|
+
return input.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "").replace(/on\w+\s*=\s*["'][^"']*["']/gi, "").replace(/javascript:/gi, "").trim();
|
|
70
|
+
}
|
|
71
|
+
function validateAndSanitizeInput(input, schema) {
|
|
72
|
+
try {
|
|
73
|
+
const validation = schema.safeParse(input);
|
|
74
|
+
if (!validation.success) {
|
|
75
|
+
return {
|
|
76
|
+
success: false,
|
|
77
|
+
error: validation.error.issues.map((i) => i.message).join(", ")
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const sanitized = sanitizeObject(validation.data);
|
|
81
|
+
return {
|
|
82
|
+
success: true,
|
|
83
|
+
data: sanitized
|
|
84
|
+
};
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return {
|
|
87
|
+
success: false,
|
|
88
|
+
error: error instanceof Error ? error.message : "Validation failed"
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function sanitizeObject(obj) {
|
|
93
|
+
if (typeof obj === "string") {
|
|
94
|
+
return sanitizeInput(obj);
|
|
95
|
+
}
|
|
96
|
+
if (Array.isArray(obj)) {
|
|
97
|
+
return obj.map(sanitizeObject);
|
|
98
|
+
}
|
|
99
|
+
if (obj && typeof obj === "object") {
|
|
100
|
+
const sanitized = {};
|
|
101
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
102
|
+
sanitized[key] = sanitizeObject(value);
|
|
103
|
+
}
|
|
104
|
+
return sanitized;
|
|
105
|
+
}
|
|
106
|
+
return obj;
|
|
107
|
+
}
|
|
108
|
+
var SECURITY_HEADERS = {
|
|
109
|
+
"X-Content-Type-Options": "nosniff",
|
|
110
|
+
"X-Frame-Options": "DENY",
|
|
111
|
+
"X-XSS-Protection": "1; mode=block",
|
|
112
|
+
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
113
|
+
"Permissions-Policy": "camera=(), microphone=(), geolocation=()"
|
|
114
|
+
};
|
|
115
|
+
function logSecurityEvent(event, details, severity = "medium") {
|
|
116
|
+
console.log(`[SECURITY-${severity.toUpperCase()}] ${event}:`, {
|
|
117
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
118
|
+
severity,
|
|
119
|
+
...details
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/RateLimiterService.ts
|
|
124
|
+
var devLogger = {
|
|
125
|
+
debug: (...args) => console.debug("[security]", ...args),
|
|
126
|
+
info: (...args) => console.log("[security]", ...args),
|
|
127
|
+
warn: (...args) => console.warn("[security]", ...args),
|
|
128
|
+
error: (...args) => console.error("[security]", ...args),
|
|
129
|
+
forService: (_name) => ({
|
|
130
|
+
debug: (...args) => console.debug("[security]", ...args),
|
|
131
|
+
info: (...args) => console.log("[security]", ...args),
|
|
132
|
+
warn: (...args) => console.warn("[security]", ...args),
|
|
133
|
+
error: (...args) => console.error("[security]", ...args)
|
|
134
|
+
})
|
|
135
|
+
};
|
|
136
|
+
var RateLimiterService = class {
|
|
137
|
+
config;
|
|
138
|
+
inMemoryStore = /* @__PURE__ */ new Map();
|
|
139
|
+
cleanupInterval = null;
|
|
140
|
+
constructor(config) {
|
|
141
|
+
this.config = {
|
|
142
|
+
windowMs: config.windowMs ?? 15 * 60 * 1e3,
|
|
143
|
+
// 15 minutes
|
|
144
|
+
maxRequests: config.maxRequests ?? 100,
|
|
145
|
+
skipSuccessfulRequests: config.skipSuccessfulRequests ?? false,
|
|
146
|
+
skipFailedRequests: config.skipFailedRequests ?? false,
|
|
147
|
+
standardHeaders: config.standardHeaders ?? true,
|
|
148
|
+
legacyHeaders: config.legacyHeaders ?? false
|
|
149
|
+
};
|
|
150
|
+
this.startCleanupInterval();
|
|
151
|
+
}
|
|
152
|
+
setPayload(_payload) {
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Check rate limit using token bucket algorithm
|
|
156
|
+
*/
|
|
157
|
+
checkTokenBucket(key, tokens = 1, capacity = 10, refillRate = 1, refillTime = 100) {
|
|
158
|
+
try {
|
|
159
|
+
const now = Date.now();
|
|
160
|
+
const bucketKey = `rate_limit:token_bucket:${key}`;
|
|
161
|
+
const current = this.inMemoryStore.get(bucketKey);
|
|
162
|
+
let currentTokens = capacity;
|
|
163
|
+
let lastRefill = now;
|
|
164
|
+
if (current) {
|
|
165
|
+
currentTokens = current.count;
|
|
166
|
+
lastRefill = current.lastRequestTime;
|
|
167
|
+
}
|
|
168
|
+
const timePassed = now - lastRefill;
|
|
169
|
+
const refillAmount = Math.floor(timePassed / refillTime) * refillRate;
|
|
170
|
+
currentTokens = Math.min(capacity, currentTokens + refillAmount);
|
|
171
|
+
if (currentTokens >= tokens) {
|
|
172
|
+
currentTokens = currentTokens - tokens;
|
|
173
|
+
this.inMemoryStore.set(bucketKey, {
|
|
174
|
+
count: currentTokens,
|
|
175
|
+
resetTime: now + refillTime,
|
|
176
|
+
lastRequestTime: now
|
|
177
|
+
});
|
|
178
|
+
devLogger.debug("Rate limit check", {
|
|
179
|
+
key,
|
|
180
|
+
algorithm: "token_bucket",
|
|
181
|
+
allowed: true,
|
|
182
|
+
remaining: currentTokens,
|
|
183
|
+
resetTime: now + refillTime,
|
|
184
|
+
tokens,
|
|
185
|
+
capacity,
|
|
186
|
+
refillRate
|
|
187
|
+
});
|
|
188
|
+
return {
|
|
189
|
+
allowed: true,
|
|
190
|
+
remaining: currentTokens,
|
|
191
|
+
resetTime: now + refillTime
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
const resetTime = lastRefill + refillTime;
|
|
195
|
+
devLogger.warn("Rate limit blocked", {
|
|
196
|
+
key,
|
|
197
|
+
algorithm: "token_bucket",
|
|
198
|
+
remaining: currentTokens,
|
|
199
|
+
resetTime,
|
|
200
|
+
tokens
|
|
201
|
+
});
|
|
202
|
+
return {
|
|
203
|
+
allowed: false,
|
|
204
|
+
remaining: currentTokens,
|
|
205
|
+
resetTime,
|
|
206
|
+
retryAfter: Math.ceil((resetTime - now) / 1e3)
|
|
207
|
+
};
|
|
208
|
+
} catch (error) {
|
|
209
|
+
devLogger.error("Rate limiter error", error instanceof Error ? error : void 0, {
|
|
210
|
+
key,
|
|
211
|
+
algorithm: "token_bucket",
|
|
212
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
213
|
+
});
|
|
214
|
+
return {
|
|
215
|
+
allowed: true,
|
|
216
|
+
remaining: capacity,
|
|
217
|
+
resetTime: Date.now() + refillTime
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Check rate limit using sliding window algorithm
|
|
223
|
+
*/
|
|
224
|
+
checkSlidingWindow(key, windowMs = this.config.windowMs, maxRequests = this.config.maxRequests) {
|
|
225
|
+
try {
|
|
226
|
+
const now = Date.now();
|
|
227
|
+
const windowKey = `rate_limit:sliding_window:${key}`;
|
|
228
|
+
const current = this.inMemoryStore.get(windowKey);
|
|
229
|
+
let requests = [];
|
|
230
|
+
if (current) {
|
|
231
|
+
requests = [current.count];
|
|
232
|
+
}
|
|
233
|
+
const windowStart = now - windowMs;
|
|
234
|
+
requests = requests.filter((timestamp) => timestamp >= windowStart);
|
|
235
|
+
if (requests.length < maxRequests) {
|
|
236
|
+
requests.push(now);
|
|
237
|
+
this.inMemoryStore.set(windowKey, {
|
|
238
|
+
count: requests.length,
|
|
239
|
+
resetTime: now + windowMs,
|
|
240
|
+
lastRequestTime: now
|
|
241
|
+
});
|
|
242
|
+
devLogger.debug("Rate limit check", {
|
|
243
|
+
key,
|
|
244
|
+
algorithm: "sliding_window",
|
|
245
|
+
allowed: true,
|
|
246
|
+
remaining: maxRequests - requests.length,
|
|
247
|
+
resetTime: now + windowMs
|
|
248
|
+
});
|
|
249
|
+
return {
|
|
250
|
+
allowed: true,
|
|
251
|
+
remaining: maxRequests - requests.length,
|
|
252
|
+
resetTime: now + windowMs
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
const oldest = Math.min(...requests);
|
|
256
|
+
const resetTime = oldest + windowMs;
|
|
257
|
+
devLogger.warn("Rate limit blocked", {
|
|
258
|
+
key,
|
|
259
|
+
algorithm: "sliding_window",
|
|
260
|
+
remaining: 0,
|
|
261
|
+
resetTime
|
|
262
|
+
});
|
|
263
|
+
return {
|
|
264
|
+
allowed: false,
|
|
265
|
+
remaining: 0,
|
|
266
|
+
resetTime,
|
|
267
|
+
retryAfter: Math.ceil((resetTime - now) / 1e3)
|
|
268
|
+
};
|
|
269
|
+
} catch (error) {
|
|
270
|
+
devLogger.error("Rate limiter error", error instanceof Error ? error : void 0, {
|
|
271
|
+
key,
|
|
272
|
+
algorithm: "sliding_window",
|
|
273
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
274
|
+
});
|
|
275
|
+
return {
|
|
276
|
+
allowed: true,
|
|
277
|
+
remaining: maxRequests,
|
|
278
|
+
resetTime: Date.now() + windowMs
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Check rate limit using leaky bucket algorithm
|
|
284
|
+
*/
|
|
285
|
+
checkLeakyBucket(key, capacity = 10, leakRate = 1, leakTime = 100) {
|
|
286
|
+
try {
|
|
287
|
+
const now = Date.now();
|
|
288
|
+
const bucketKey = `rate_limit:leaky_bucket:${key}`;
|
|
289
|
+
const current = this.inMemoryStore.get(bucketKey);
|
|
290
|
+
let currentLevel = 0;
|
|
291
|
+
let lastLeak = now;
|
|
292
|
+
if (current) {
|
|
293
|
+
currentLevel = current.count;
|
|
294
|
+
lastLeak = current.lastRequestTime;
|
|
295
|
+
}
|
|
296
|
+
const timePassed = now - lastLeak;
|
|
297
|
+
const leakAmount = Math.floor(timePassed / leakTime) * leakRate;
|
|
298
|
+
currentLevel = Math.max(0, currentLevel - leakAmount);
|
|
299
|
+
if (currentLevel < capacity) {
|
|
300
|
+
currentLevel++;
|
|
301
|
+
this.inMemoryStore.set(bucketKey, {
|
|
302
|
+
count: currentLevel,
|
|
303
|
+
resetTime: now + leakTime,
|
|
304
|
+
lastRequestTime: now
|
|
305
|
+
});
|
|
306
|
+
devLogger.debug("Rate limit check", {
|
|
307
|
+
key,
|
|
308
|
+
algorithm: "leaky_bucket",
|
|
309
|
+
allowed: true,
|
|
310
|
+
remaining: capacity - currentLevel,
|
|
311
|
+
resetTime: now + leakTime
|
|
312
|
+
});
|
|
313
|
+
return {
|
|
314
|
+
allowed: true,
|
|
315
|
+
remaining: capacity - currentLevel,
|
|
316
|
+
resetTime: now + leakTime
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
const resetTime = lastLeak + leakTime;
|
|
320
|
+
devLogger.warn("Rate limit blocked", {
|
|
321
|
+
key,
|
|
322
|
+
algorithm: "leaky_bucket",
|
|
323
|
+
remaining: 0,
|
|
324
|
+
resetTime
|
|
325
|
+
});
|
|
326
|
+
return {
|
|
327
|
+
allowed: false,
|
|
328
|
+
remaining: 0,
|
|
329
|
+
resetTime,
|
|
330
|
+
retryAfter: Math.ceil((resetTime - now) / 1e3)
|
|
331
|
+
};
|
|
332
|
+
} catch (error) {
|
|
333
|
+
devLogger.error("Rate limiter error", error instanceof Error ? error : void 0, {
|
|
334
|
+
key,
|
|
335
|
+
algorithm: "leaky_bucket",
|
|
336
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
337
|
+
});
|
|
338
|
+
return {
|
|
339
|
+
allowed: true,
|
|
340
|
+
remaining: capacity,
|
|
341
|
+
resetTime: Date.now() + leakTime
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Reset rate limit for a key
|
|
347
|
+
*/
|
|
348
|
+
resetRateLimit(key) {
|
|
349
|
+
const patterns = [
|
|
350
|
+
`rate_limit:token_bucket:${key}`,
|
|
351
|
+
`rate_limit:sliding_window:${key}`,
|
|
352
|
+
`rate_limit:leaky_bucket:${key}`
|
|
353
|
+
];
|
|
354
|
+
for (const pattern of patterns) {
|
|
355
|
+
this.inMemoryStore.delete(pattern);
|
|
356
|
+
}
|
|
357
|
+
devLogger.debug("Rate limit reset", { key });
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Get current rate limit state
|
|
361
|
+
*/
|
|
362
|
+
getRateLimitState(key) {
|
|
363
|
+
const patterns = [
|
|
364
|
+
`rate_limit:token_bucket:${key}`,
|
|
365
|
+
`rate_limit:sliding_window:${key}`,
|
|
366
|
+
`rate_limit:leaky_bucket:${key}`
|
|
367
|
+
];
|
|
368
|
+
for (const pattern of patterns) {
|
|
369
|
+
const state = this.inMemoryStore.get(pattern);
|
|
370
|
+
if (state) {
|
|
371
|
+
return state;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Update rate limit state
|
|
378
|
+
*/
|
|
379
|
+
updateRateLimitState(key, state) {
|
|
380
|
+
this.inMemoryStore.set(key, state);
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Get rate limiter statistics
|
|
384
|
+
*/
|
|
385
|
+
getStats() {
|
|
386
|
+
return {
|
|
387
|
+
totalKeys: this.inMemoryStore.size,
|
|
388
|
+
memoryUsage: this.inMemoryStore.size
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Clean up expired entries
|
|
393
|
+
*/
|
|
394
|
+
cleanup() {
|
|
395
|
+
const now = Date.now();
|
|
396
|
+
const maxAge = 24 * 60 * 60 * 1e3;
|
|
397
|
+
for (const [key, state] of this.inMemoryStore.entries()) {
|
|
398
|
+
if (now - state.lastRequestTime > maxAge) {
|
|
399
|
+
this.inMemoryStore.delete(key);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
devLogger.debug("Cleaned up expired rate limit entries", {
|
|
403
|
+
remaining: this.inMemoryStore.size
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Start cleanup interval
|
|
408
|
+
*/
|
|
409
|
+
startCleanupInterval() {
|
|
410
|
+
if (this.cleanupInterval) {
|
|
411
|
+
clearInterval(this.cleanupInterval);
|
|
412
|
+
}
|
|
413
|
+
this.cleanupInterval = setInterval(
|
|
414
|
+
() => {
|
|
415
|
+
this.cleanup();
|
|
416
|
+
},
|
|
417
|
+
60 * 60 * 1e3
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Stop cleanup interval
|
|
422
|
+
*/
|
|
423
|
+
stopCleanupInterval() {
|
|
424
|
+
if (this.cleanupInterval) {
|
|
425
|
+
clearInterval(this.cleanupInterval);
|
|
426
|
+
this.cleanupInterval = null;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Dispose of the service
|
|
431
|
+
*/
|
|
432
|
+
dispose() {
|
|
433
|
+
this.stopCleanupInterval();
|
|
434
|
+
this.inMemoryStore.clear();
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
var getRateLimiterService = (config) => {
|
|
438
|
+
return new RateLimiterService(config ?? {});
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
// src/RateLimitingService.ts
|
|
442
|
+
import { headers } from "next/headers";
|
|
443
|
+
var RATE_LIMITS2 = { default: { limit: 100, windowMs: 6e4 } };
|
|
444
|
+
var generateRateLimitKey2 = (id) => `rate:${id}`;
|
|
445
|
+
var generateIPRateLimitKey2 = (ip) => `rate:ip:${ip}`;
|
|
446
|
+
async function checkRateLimit2(_key, _limit, _windowMs) {
|
|
447
|
+
return { allowed: true, remaining: _limit, resetTime: Date.now() + _windowMs };
|
|
448
|
+
}
|
|
449
|
+
async function withRateLimit(action, config) {
|
|
450
|
+
return async (...args) => {
|
|
451
|
+
const headersList = await headers();
|
|
452
|
+
const ip = headersList.get("x-forwarded-for") ?? headersList.get("x-real-ip") ?? "unknown";
|
|
453
|
+
const key = config.customKey ?? (config.userId ? generateRateLimitKey2(config.userId) : generateIPRateLimitKey2(ip));
|
|
454
|
+
const rateLimitResult = await checkRateLimit2(
|
|
455
|
+
key,
|
|
456
|
+
RATE_LIMITS2[config.type].limit,
|
|
457
|
+
RATE_LIMITS2[config.type].windowMs
|
|
458
|
+
);
|
|
459
|
+
if (!rateLimitResult.allowed) {
|
|
460
|
+
throw new Error(
|
|
461
|
+
`Rate limit exceeded. Try again in ${Math.ceil((rateLimitResult.resetTime - Date.now()) / 1e3)} seconds.`
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
return await action(...args);
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
function RateLimited(type, userId) {
|
|
468
|
+
return (_target, _propertyKey, descriptor) => {
|
|
469
|
+
const originalMethod = descriptor.value;
|
|
470
|
+
descriptor.value = async function(...args) {
|
|
471
|
+
const headersList = await headers();
|
|
472
|
+
const ip = headersList.get("x-forwarded-for") ?? headersList.get("x-real-ip") ?? "unknown";
|
|
473
|
+
const key = userId ? generateRateLimitKey2(userId) : generateIPRateLimitKey2(ip);
|
|
474
|
+
const rateLimitResult = await checkRateLimit2(
|
|
475
|
+
key,
|
|
476
|
+
RATE_LIMITS2[type].limit,
|
|
477
|
+
RATE_LIMITS2[type].windowMs
|
|
478
|
+
);
|
|
479
|
+
if (!rateLimitResult.allowed) {
|
|
480
|
+
throw new Error(
|
|
481
|
+
`Rate limit exceeded. Try again in ${Math.ceil((rateLimitResult.resetTime - Date.now()) / 1e3)} seconds.`
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
return await originalMethod.apply(this, args);
|
|
485
|
+
};
|
|
486
|
+
return descriptor;
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
async function getClientIP() {
|
|
490
|
+
const headersList = await headers();
|
|
491
|
+
return headersList.get("x-forwarded-for") ?? headersList.get("x-real-ip") ?? "unknown";
|
|
492
|
+
}
|
|
493
|
+
async function isTrustedSource() {
|
|
494
|
+
const headersList = await headers();
|
|
495
|
+
const userAgent = headersList.get("user-agent") || "";
|
|
496
|
+
const origin = headersList.get("origin") || "";
|
|
497
|
+
const botPatterns = [/bot/i, /crawler/i, /spider/i, /scraper/i, /curl/i, /wget/i];
|
|
498
|
+
const isBot = botPatterns.some((pattern) => pattern.test(userAgent));
|
|
499
|
+
const trustedOrigins = [
|
|
500
|
+
process.env.NEXT_PUBLIC_APP_URL,
|
|
501
|
+
"http://localhost:3000",
|
|
502
|
+
"https://localhost:3000"
|
|
503
|
+
].filter(Boolean);
|
|
504
|
+
const isTrustedOrigin = trustedOrigins.includes(origin);
|
|
505
|
+
return !isBot && isTrustedOrigin;
|
|
506
|
+
}
|
|
507
|
+
async function checkUserRateLimit(userId, action, customConfig) {
|
|
508
|
+
const config = { ...RATE_LIMITS2[action], ...customConfig };
|
|
509
|
+
const key = generateRateLimitKey2(userId);
|
|
510
|
+
return await checkRateLimit2(key, config.limit, config.windowMs);
|
|
511
|
+
}
|
|
512
|
+
async function checkAnonymousRateLimit(action, customConfig) {
|
|
513
|
+
const ip = await getClientIP();
|
|
514
|
+
const config = { ...RATE_LIMITS2[action], ...customConfig };
|
|
515
|
+
const key = generateIPRateLimitKey2(ip);
|
|
516
|
+
return await checkRateLimit2(key, config.limit, config.windowMs);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// src/CSRFService.ts
|
|
520
|
+
import { createHmac, randomBytes } from "crypto";
|
|
521
|
+
var CSRFService = class {
|
|
522
|
+
constructor(payload, options = {}) {
|
|
523
|
+
this.payload = payload;
|
|
524
|
+
this.options = {
|
|
525
|
+
algorithm: options.algorithm ?? "sha256",
|
|
526
|
+
encoding: options.encoding ?? "hex",
|
|
527
|
+
tokenLength: options.tokenLength ?? 32,
|
|
528
|
+
tokenExpiry: options.tokenExpiry ?? 24 * 60 * 60 * 1e3
|
|
529
|
+
// 24 hours
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
options;
|
|
533
|
+
async generateCsrfToken(sessionId) {
|
|
534
|
+
try {
|
|
535
|
+
const token = randomBytes(this.options.tokenLength);
|
|
536
|
+
const hmac = createHmac(this.options.algorithm, process.env.PAYLOAD_SECRET ?? "");
|
|
537
|
+
hmac.update(sessionId);
|
|
538
|
+
hmac.update(token);
|
|
539
|
+
const signature = hmac.digest(this.options.encoding);
|
|
540
|
+
const csrfToken = `${token.toString(this.options.encoding)}.${signature}`;
|
|
541
|
+
this.payload.logger.debug("CSRF token generated successfully");
|
|
542
|
+
return csrfToken;
|
|
543
|
+
} catch (error) {
|
|
544
|
+
this.payload.logger.error("Failed to generate CSRF token", error);
|
|
545
|
+
throw error;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
async validateCsrfToken(token, sessionId) {
|
|
549
|
+
try {
|
|
550
|
+
const [tokenPart, signature] = token.split(".");
|
|
551
|
+
if (!tokenPart || !signature) {
|
|
552
|
+
this.payload.logger.warn("Invalid CSRF token format");
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
const hmac = createHmac(this.options.algorithm, process.env.PAYLOAD_SECRET ?? "");
|
|
556
|
+
hmac.update(sessionId);
|
|
557
|
+
hmac.update(Buffer.from(tokenPart, this.options.encoding));
|
|
558
|
+
const expectedSignature = hmac.digest(this.options.encoding);
|
|
559
|
+
const isValid = signature === expectedSignature;
|
|
560
|
+
if (isValid) {
|
|
561
|
+
this.payload.logger.debug("CSRF token validated successfully");
|
|
562
|
+
} else {
|
|
563
|
+
this.payload.logger.warn("CSRF token validation failed");
|
|
564
|
+
}
|
|
565
|
+
return isValid;
|
|
566
|
+
} catch (error) {
|
|
567
|
+
this.payload.logger.error("Failed to validate CSRF token", error);
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
// src/EncryptionService.ts
|
|
574
|
+
import { InfrastructureService } from "@revealui/core";
|
|
575
|
+
import {
|
|
576
|
+
createCipheriv,
|
|
577
|
+
createDecipheriv,
|
|
578
|
+
randomBytes as randomBytes2,
|
|
579
|
+
scrypt as scryptCallback
|
|
580
|
+
} from "crypto";
|
|
581
|
+
import { promisify } from "util";
|
|
582
|
+
var scryptAsync = promisify(scryptCallback);
|
|
583
|
+
function isAuthenticatedCipher(cipher) {
|
|
584
|
+
return typeof cipher.getAuthTag === "function";
|
|
585
|
+
}
|
|
586
|
+
function isAuthenticatedDecipher(decipher) {
|
|
587
|
+
return typeof decipher.setAuthTag === "function";
|
|
588
|
+
}
|
|
589
|
+
var EncryptionService = class extends InfrastructureService {
|
|
590
|
+
options;
|
|
591
|
+
constructor(payload, options = {}) {
|
|
592
|
+
super(payload, "EncryptionService");
|
|
593
|
+
this.options = {
|
|
594
|
+
algorithm: options.algorithm ?? "aes-256-gcm",
|
|
595
|
+
keyLength: options.keyLength ?? 32,
|
|
596
|
+
saltLength: options.saltLength ?? 16,
|
|
597
|
+
ivLength: options.ivLength ?? 12,
|
|
598
|
+
iterations: options.iterations ?? 1e5,
|
|
599
|
+
encoding: options.encoding ?? "hex"
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Encrypts data using the configured algorithm with comprehensive error handling.
|
|
604
|
+
*/
|
|
605
|
+
async encrypt(data, customOptions) {
|
|
606
|
+
return this.executeInfrastructureOperation(async () => {
|
|
607
|
+
if (typeof data !== "string" || data.length === 0) {
|
|
608
|
+
throw new Error("Data to encrypt must be a non-empty string");
|
|
609
|
+
}
|
|
610
|
+
const effectiveOptions = { ...this.options, ...customOptions };
|
|
611
|
+
this.logger.debug("Starting data encryption", {
|
|
612
|
+
algorithm: effectiveOptions.algorithm,
|
|
613
|
+
dataLength: data.length,
|
|
614
|
+
serviceName: this.serviceName,
|
|
615
|
+
operation: "encrypt"
|
|
616
|
+
});
|
|
617
|
+
const salt = randomBytes2(effectiveOptions.saltLength);
|
|
618
|
+
const iv = randomBytes2(effectiveOptions.ivLength);
|
|
619
|
+
const key = await this.deriveKey(salt, effectiveOptions);
|
|
620
|
+
const cipher = createCipheriv(effectiveOptions.algorithm, key, iv);
|
|
621
|
+
const encrypted = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
|
|
622
|
+
if (isAuthenticatedCipher(cipher) !== true) {
|
|
623
|
+
throw new Error("Cipher does not support authentication");
|
|
624
|
+
}
|
|
625
|
+
const authTag = cipher.getAuthTag();
|
|
626
|
+
const result = {
|
|
627
|
+
data: encrypted.toString(effectiveOptions.encoding),
|
|
628
|
+
metadata: {
|
|
629
|
+
algorithm: effectiveOptions.algorithm,
|
|
630
|
+
iv: iv.toString(effectiveOptions.encoding),
|
|
631
|
+
authTag: authTag.toString(effectiveOptions.encoding),
|
|
632
|
+
salt: salt.toString(effectiveOptions.encoding)
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
this.logger.debug("Data encrypted successfully", {
|
|
636
|
+
algorithm: effectiveOptions.algorithm,
|
|
637
|
+
originalLength: data.length,
|
|
638
|
+
encryptedLength: result.data.length,
|
|
639
|
+
serviceName: this.serviceName
|
|
640
|
+
});
|
|
641
|
+
return result;
|
|
642
|
+
}, "encrypt");
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Decrypts data using the metadata provided with comprehensive validation.
|
|
646
|
+
*/
|
|
647
|
+
async decrypt(encryptedData) {
|
|
648
|
+
return this.executeInfrastructureOperation(async () => {
|
|
649
|
+
if (!encryptedData || !encryptedData.data || !encryptedData.metadata) {
|
|
650
|
+
throw new Error("Invalid encrypted data structure");
|
|
651
|
+
}
|
|
652
|
+
const { data, metadata } = encryptedData;
|
|
653
|
+
const requiredFields = ["algorithm", "iv", "authTag", "salt"];
|
|
654
|
+
for (const field of requiredFields) {
|
|
655
|
+
const fieldValue = metadata[field];
|
|
656
|
+
if (fieldValue === void 0 || fieldValue === null || fieldValue === "") {
|
|
657
|
+
throw new Error(`Missing required encryption metadata field: ${field}`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
this.logger.debug("Starting data decryption", {
|
|
661
|
+
algorithm: metadata.algorithm,
|
|
662
|
+
encryptedLength: data.length,
|
|
663
|
+
serviceName: this.serviceName,
|
|
664
|
+
operation: "decrypt"
|
|
665
|
+
});
|
|
666
|
+
const key = await this.deriveKey(Buffer.from(metadata.salt, this.options.encoding), {
|
|
667
|
+
...this.options,
|
|
668
|
+
algorithm: metadata.algorithm
|
|
669
|
+
});
|
|
670
|
+
const decipher = createDecipheriv(
|
|
671
|
+
metadata.algorithm,
|
|
672
|
+
key,
|
|
673
|
+
Buffer.from(metadata.iv, this.options.encoding)
|
|
674
|
+
);
|
|
675
|
+
if (isAuthenticatedDecipher(decipher) !== true) {
|
|
676
|
+
throw new Error("Decipher does not support authentication");
|
|
677
|
+
}
|
|
678
|
+
decipher.setAuthTag(Buffer.from(metadata.authTag, this.options.encoding));
|
|
679
|
+
const decrypted = Buffer.concat([
|
|
680
|
+
decipher.update(Buffer.from(data, this.options.encoding)),
|
|
681
|
+
decipher.final()
|
|
682
|
+
]);
|
|
683
|
+
const result = decrypted.toString("utf8");
|
|
684
|
+
this.logger.debug("Data decrypted successfully", {
|
|
685
|
+
algorithm: metadata.algorithm,
|
|
686
|
+
encryptedLength: data.length,
|
|
687
|
+
decryptedLength: result.length,
|
|
688
|
+
serviceName: this.serviceName
|
|
689
|
+
});
|
|
690
|
+
return result;
|
|
691
|
+
}, "decrypt");
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Encrypts multiple data items in batch with optimized processing.
|
|
695
|
+
*/
|
|
696
|
+
async encryptBatch(dataItems, customOptions) {
|
|
697
|
+
return this.executeInfrastructureOperation(async () => {
|
|
698
|
+
if (!Array.isArray(dataItems) || dataItems.length === 0) {
|
|
699
|
+
throw new Error("Data items array must contain at least one item");
|
|
700
|
+
}
|
|
701
|
+
this.logger.info("Starting batch encryption", {
|
|
702
|
+
itemCount: dataItems.length,
|
|
703
|
+
serviceName: this.serviceName,
|
|
704
|
+
operation: "encryptBatch"
|
|
705
|
+
});
|
|
706
|
+
const successful = [];
|
|
707
|
+
const failed = [];
|
|
708
|
+
const encryptionPromises = dataItems.map(async (data) => {
|
|
709
|
+
try {
|
|
710
|
+
const result = await this.encrypt(data, customOptions);
|
|
711
|
+
if (result.success && result.data) {
|
|
712
|
+
successful.push(result.data);
|
|
713
|
+
} else {
|
|
714
|
+
failed.push({
|
|
715
|
+
data,
|
|
716
|
+
error: result.error ?? "Unknown encryption error"
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
} catch (error) {
|
|
720
|
+
failed.push({
|
|
721
|
+
data,
|
|
722
|
+
error: error instanceof Error ? error.message : "Unknown encryption error"
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
await Promise.allSettled(encryptionPromises);
|
|
727
|
+
this.logger.info("Batch encryption completed", {
|
|
728
|
+
totalItems: dataItems.length,
|
|
729
|
+
successful: successful.length,
|
|
730
|
+
failed: failed.length,
|
|
731
|
+
serviceName: this.serviceName
|
|
732
|
+
});
|
|
733
|
+
return { successful, failed };
|
|
734
|
+
}, "encryptBatch");
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Generates cryptographically secure random data.
|
|
738
|
+
*/
|
|
739
|
+
async generateRandomData(length = 32, encoding = "hex") {
|
|
740
|
+
return this.executeInfrastructureOperation(async () => {
|
|
741
|
+
if (length <= 0 || length > 1024) {
|
|
742
|
+
throw new Error("Random data length must be between 1 and 1024 bytes");
|
|
743
|
+
}
|
|
744
|
+
this.logger.debug("Generating random data", {
|
|
745
|
+
length,
|
|
746
|
+
encoding,
|
|
747
|
+
serviceName: this.serviceName,
|
|
748
|
+
operation: "generateRandomData"
|
|
749
|
+
});
|
|
750
|
+
const randomData = randomBytes2(length);
|
|
751
|
+
const result = randomData.toString(encoding);
|
|
752
|
+
this.logger.debug("Random data generated successfully", {
|
|
753
|
+
length,
|
|
754
|
+
encoding,
|
|
755
|
+
outputLength: result.length,
|
|
756
|
+
serviceName: this.serviceName
|
|
757
|
+
});
|
|
758
|
+
return result;
|
|
759
|
+
}, "generateRandomData");
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Validates the integrity of encrypted data by attempting decryption.
|
|
763
|
+
*/
|
|
764
|
+
async validateEncryptedData(encryptedData) {
|
|
765
|
+
return this.executeInfrastructureOperation(async () => {
|
|
766
|
+
if (!encryptedData) {
|
|
767
|
+
throw new Error("Encrypted data is required");
|
|
768
|
+
}
|
|
769
|
+
this.logger.debug("Validating encrypted data integrity", {
|
|
770
|
+
algorithm: encryptedData.metadata.algorithm,
|
|
771
|
+
serviceName: this.serviceName,
|
|
772
|
+
operation: "validateEncryptedData"
|
|
773
|
+
});
|
|
774
|
+
try {
|
|
775
|
+
const decryptResult = await this.decrypt(encryptedData);
|
|
776
|
+
const valid = decryptResult.success && !!decryptResult.data;
|
|
777
|
+
this.logger.debug("Encrypted data validation completed", {
|
|
778
|
+
valid,
|
|
779
|
+
serviceName: this.serviceName
|
|
780
|
+
});
|
|
781
|
+
return valid ? { valid, error: void 0 } : {
|
|
782
|
+
valid,
|
|
783
|
+
error: decryptResult.error ?? "Decryption failed"
|
|
784
|
+
};
|
|
785
|
+
} catch (error) {
|
|
786
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown validation error";
|
|
787
|
+
this.logger.debug("Encrypted data validation failed", {
|
|
788
|
+
error: errorMessage,
|
|
789
|
+
serviceName: this.serviceName
|
|
790
|
+
});
|
|
791
|
+
return { valid: false, error: errorMessage };
|
|
792
|
+
}
|
|
793
|
+
}, "validateEncryptedData");
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Derives encryption key from password and salt using PBKDF2.
|
|
797
|
+
* @private
|
|
798
|
+
*/
|
|
799
|
+
async deriveKey(salt, options = {}) {
|
|
800
|
+
const effectiveOptions = { ...this.options, ...options };
|
|
801
|
+
const secret = process.env.PAYLOAD_SECRET;
|
|
802
|
+
if (!secret || secret === "") {
|
|
803
|
+
throw new Error("PAYLOAD_SECRET environment variable is required for encryption");
|
|
804
|
+
}
|
|
805
|
+
return scryptAsync(secret, salt, effectiveOptions.keyLength);
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* @inheritdoc
|
|
809
|
+
*/
|
|
810
|
+
async onInitialize() {
|
|
811
|
+
this.logger.info("Initializing EncryptionService", {
|
|
812
|
+
algorithm: this.options.algorithm,
|
|
813
|
+
keyLength: this.options.keyLength,
|
|
814
|
+
serviceName: this.serviceName
|
|
815
|
+
});
|
|
816
|
+
if (!process.env.PAYLOAD_SECRET) {
|
|
817
|
+
throw new Error("PAYLOAD_SECRET environment variable is required for EncryptionService");
|
|
818
|
+
}
|
|
819
|
+
try {
|
|
820
|
+
const testData = "encryption-test-data";
|
|
821
|
+
const encrypted = await this.encrypt(testData);
|
|
822
|
+
if (!encrypted.success || !encrypted.data) {
|
|
823
|
+
throw new Error("Encryption test failed");
|
|
824
|
+
}
|
|
825
|
+
const decrypted = await this.decrypt(encrypted.data);
|
|
826
|
+
if (!decrypted.success || decrypted.data !== testData) {
|
|
827
|
+
throw new Error("Decryption test failed");
|
|
828
|
+
}
|
|
829
|
+
this.logger.debug("EncryptionService initialization test passed", {
|
|
830
|
+
serviceName: this.serviceName
|
|
831
|
+
});
|
|
832
|
+
} catch (error) {
|
|
833
|
+
throw new Error(
|
|
834
|
+
`EncryptionService initialization failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* @inheritdoc
|
|
840
|
+
*/
|
|
841
|
+
async onCleanup() {
|
|
842
|
+
this.logger.info("Cleaning up EncryptionService", {
|
|
843
|
+
serviceName: this.serviceName
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* @inheritdoc
|
|
848
|
+
*/
|
|
849
|
+
async onHealthCheck() {
|
|
850
|
+
try {
|
|
851
|
+
const testData = "health-check-data";
|
|
852
|
+
const encrypted = await this.encrypt(testData);
|
|
853
|
+
if (!encrypted.success || !encrypted.data) {
|
|
854
|
+
throw new Error("Health check encryption failed");
|
|
855
|
+
}
|
|
856
|
+
const decrypted = await this.decrypt(encrypted.data);
|
|
857
|
+
if (!decrypted.success || decrypted.data !== testData) {
|
|
858
|
+
throw new Error("Health check decryption failed");
|
|
859
|
+
}
|
|
860
|
+
} catch (error) {
|
|
861
|
+
throw new Error(
|
|
862
|
+
`EncryptionService health check failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
// src/SecurityMonitoringService.ts
|
|
869
|
+
var devLogger2 = {
|
|
870
|
+
info: (...args) => console.log("[security]", ...args),
|
|
871
|
+
warn: (...args) => console.warn("[security]", ...args),
|
|
872
|
+
error: (...args) => console.error("[security]", ...args)
|
|
873
|
+
};
|
|
874
|
+
var createSecurityEvent = (type, severity, message, options) => {
|
|
875
|
+
const baseEvent = {
|
|
876
|
+
id: crypto.randomUUID(),
|
|
877
|
+
type,
|
|
878
|
+
severity,
|
|
879
|
+
message,
|
|
880
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
881
|
+
...options?.metadata ? { metadata: options.metadata } : {},
|
|
882
|
+
...options?.userId !== void 0 ? { userId: options.userId } : {},
|
|
883
|
+
...options?.ipAddress !== void 0 ? { ipAddress: options.ipAddress } : {},
|
|
884
|
+
...options?.userAgent !== void 0 ? { userAgent: options.userAgent } : {}
|
|
885
|
+
};
|
|
886
|
+
return baseEvent;
|
|
887
|
+
};
|
|
888
|
+
var calculateSecurityMetrics = (events2) => {
|
|
889
|
+
if (events2.length === 0) {
|
|
890
|
+
return {
|
|
891
|
+
totalEvents: 0,
|
|
892
|
+
eventsByType: {},
|
|
893
|
+
eventsBySeverity: {},
|
|
894
|
+
averageResponseTime: 0
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
const eventsByType = events2.reduce((acc, event) => {
|
|
898
|
+
acc[event.type] = (acc[event.type] || 0) + 1;
|
|
899
|
+
return acc;
|
|
900
|
+
}, {});
|
|
901
|
+
const eventsBySeverity = events2.reduce((acc, event) => {
|
|
902
|
+
acc[event.severity] = (acc[event.severity] || 0) + 1;
|
|
903
|
+
return acc;
|
|
904
|
+
}, {});
|
|
905
|
+
const lastEventTime = events2.length > 0 ? new Date(Math.max(...events2.map((e) => e.timestamp.getTime()))) : void 0;
|
|
906
|
+
return {
|
|
907
|
+
totalEvents: events2.length,
|
|
908
|
+
eventsByType,
|
|
909
|
+
eventsBySeverity,
|
|
910
|
+
averageResponseTime: 0,
|
|
911
|
+
// Would be calculated from actual response times
|
|
912
|
+
...lastEventTime !== void 0 ? { lastEventTime } : {}
|
|
913
|
+
};
|
|
914
|
+
};
|
|
915
|
+
var shouldTriggerAlert = (event, thresholds) => {
|
|
916
|
+
const now = /* @__PURE__ */ new Date();
|
|
917
|
+
const timeWindowStart = new Date(now.getTime() - thresholds.timeWindowMs);
|
|
918
|
+
const recentEvents = events.filter(
|
|
919
|
+
(event2) => event2.timestamp >= timeWindowStart && event2.timestamp <= now
|
|
920
|
+
);
|
|
921
|
+
const criticalCount = recentEvents.filter((e) => e.severity === "critical").length;
|
|
922
|
+
const highCount = recentEvents.filter((e) => e.severity === "high").length;
|
|
923
|
+
const mediumCount = recentEvents.filter((e) => e.severity === "medium").length;
|
|
924
|
+
if (criticalCount >= thresholds.criticalCount) return true;
|
|
925
|
+
if (highCount >= thresholds.highCount) return true;
|
|
926
|
+
if (mediumCount >= thresholds.mediumCount) return true;
|
|
927
|
+
if (event.severity === "critical") return true;
|
|
928
|
+
return false;
|
|
929
|
+
};
|
|
930
|
+
var createSecurityAlert = (event, alertType = "threshold_exceeded") => ({
|
|
931
|
+
id: crypto.randomUUID(),
|
|
932
|
+
type: alertType,
|
|
933
|
+
severity: event.severity,
|
|
934
|
+
message: `Security alert: ${event.message}`,
|
|
935
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
936
|
+
metadata: {
|
|
937
|
+
originalEventId: event.id,
|
|
938
|
+
originalEventType: event.type,
|
|
939
|
+
...event.metadata
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
var filterEvents = (events2, criteria) => {
|
|
943
|
+
return events2.filter((event) => {
|
|
944
|
+
if (criteria.type && event.type !== criteria.type) return false;
|
|
945
|
+
if (criteria.severity && event.severity !== criteria.severity) return false;
|
|
946
|
+
if (criteria.userId && event.userId !== criteria.userId) return false;
|
|
947
|
+
if (criteria.timeRange) {
|
|
948
|
+
const eventTime = event.timestamp.getTime();
|
|
949
|
+
const startTime = criteria.timeRange.start.getTime();
|
|
950
|
+
const endTime = criteria.timeRange.end.getTime();
|
|
951
|
+
if (eventTime < startTime || eventTime > endTime) return false;
|
|
952
|
+
}
|
|
953
|
+
return true;
|
|
954
|
+
});
|
|
955
|
+
};
|
|
956
|
+
var events = [];
|
|
957
|
+
var alerts = [];
|
|
958
|
+
var addSecurityEvent = (event) => {
|
|
959
|
+
events.push(event);
|
|
960
|
+
devLogger2.info("Security event recorded", {
|
|
961
|
+
eventId: event.id,
|
|
962
|
+
type: event.type,
|
|
963
|
+
severity: event.severity,
|
|
964
|
+
message: event.message,
|
|
965
|
+
userId: event.userId,
|
|
966
|
+
timestamp: event.timestamp.toISOString()
|
|
967
|
+
});
|
|
968
|
+
if (shouldTriggerAlert(event, {
|
|
969
|
+
criticalCount: 1,
|
|
970
|
+
highCount: 5,
|
|
971
|
+
mediumCount: 10,
|
|
972
|
+
timeWindowMs: 6e4
|
|
973
|
+
// 1 minute
|
|
974
|
+
})) {
|
|
975
|
+
const alert = createSecurityAlert(event);
|
|
976
|
+
alerts.push(alert);
|
|
977
|
+
devLogger2.warn("Security alert triggered", {
|
|
978
|
+
alertId: alert.id,
|
|
979
|
+
eventId: event.id,
|
|
980
|
+
severity: alert.severity,
|
|
981
|
+
message: alert.message
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
var getSecurityEvents = () => [...events];
|
|
986
|
+
var getSecurityAlerts = () => [...alerts];
|
|
987
|
+
var getSecurityMetrics = () => calculateSecurityMetrics(events);
|
|
988
|
+
var clearSecurityData = () => {
|
|
989
|
+
events = [];
|
|
990
|
+
alerts = [];
|
|
991
|
+
};
|
|
992
|
+
var logAuthenticationEvent = (userId, success, options) => {
|
|
993
|
+
const eventOptions = {
|
|
994
|
+
userId,
|
|
995
|
+
...options?.ipAddress !== void 0 ? { ipAddress: options.ipAddress } : {},
|
|
996
|
+
...options?.userAgent !== void 0 ? { userAgent: options.userAgent } : {},
|
|
997
|
+
metadata: {
|
|
998
|
+
success,
|
|
999
|
+
...options?.metadata
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
const event = createSecurityEvent(
|
|
1003
|
+
"authentication",
|
|
1004
|
+
success ? "low" : "high",
|
|
1005
|
+
`Authentication ${success ? "successful" : "failed"} for user ${userId}`,
|
|
1006
|
+
eventOptions
|
|
1007
|
+
);
|
|
1008
|
+
addSecurityEvent(event);
|
|
1009
|
+
};
|
|
1010
|
+
var logAuthorizationEvent = (userId, resource, action, granted, options) => {
|
|
1011
|
+
const eventOptions = {
|
|
1012
|
+
userId,
|
|
1013
|
+
...options?.ipAddress !== void 0 ? { ipAddress: options.ipAddress } : {},
|
|
1014
|
+
...options?.userAgent !== void 0 ? { userAgent: options.userAgent } : {},
|
|
1015
|
+
metadata: {
|
|
1016
|
+
resource,
|
|
1017
|
+
action,
|
|
1018
|
+
granted,
|
|
1019
|
+
...options?.metadata
|
|
1020
|
+
}
|
|
1021
|
+
};
|
|
1022
|
+
const event = createSecurityEvent(
|
|
1023
|
+
"authorization",
|
|
1024
|
+
granted ? "low" : "medium",
|
|
1025
|
+
`Authorization ${granted ? "granted" : "denied"} for user ${userId} on ${resource}:${action}`,
|
|
1026
|
+
eventOptions
|
|
1027
|
+
);
|
|
1028
|
+
addSecurityEvent(event);
|
|
1029
|
+
};
|
|
1030
|
+
var logDataAccessEvent = (userId, collection, operation, recordId, options) => {
|
|
1031
|
+
const eventOptions = {
|
|
1032
|
+
userId,
|
|
1033
|
+
...options?.ipAddress !== void 0 ? { ipAddress: options.ipAddress } : {},
|
|
1034
|
+
...options?.userAgent !== void 0 ? { userAgent: options.userAgent } : {},
|
|
1035
|
+
metadata: {
|
|
1036
|
+
collection,
|
|
1037
|
+
operation,
|
|
1038
|
+
recordId,
|
|
1039
|
+
...options?.metadata
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
const event = createSecurityEvent(
|
|
1043
|
+
"data_access",
|
|
1044
|
+
operation === "delete" ? "medium" : "low",
|
|
1045
|
+
`Data access: ${operation} on ${collection}${recordId ? ` (${recordId})` : ""} by user ${userId}`,
|
|
1046
|
+
eventOptions
|
|
1047
|
+
);
|
|
1048
|
+
addSecurityEvent(event);
|
|
1049
|
+
};
|
|
1050
|
+
var getUserEvents = (userId) => filterEvents(events, { userId });
|
|
1051
|
+
var getEventsBySeverity = (severity) => filterEvents(events, { severity });
|
|
1052
|
+
var getEventsInTimeRange = (start, end) => filterEvents(events, { timeRange: { start, end } });
|
|
1053
|
+
var SecurityMonitoringService = {
|
|
1054
|
+
// Event management
|
|
1055
|
+
addEvent: addSecurityEvent,
|
|
1056
|
+
getEvents: getSecurityEvents,
|
|
1057
|
+
getUserEvents,
|
|
1058
|
+
getEventsBySeverity,
|
|
1059
|
+
getEventsInTimeRange,
|
|
1060
|
+
// Alert management
|
|
1061
|
+
getAlerts: getSecurityAlerts,
|
|
1062
|
+
// Metrics
|
|
1063
|
+
getMetrics: getSecurityMetrics,
|
|
1064
|
+
// Utility functions
|
|
1065
|
+
logAuthenticationEvent,
|
|
1066
|
+
logAuthorizationEvent,
|
|
1067
|
+
logDataAccessEvent,
|
|
1068
|
+
// Testing utilities
|
|
1069
|
+
clearData: clearSecurityData,
|
|
1070
|
+
// Pure functions for composition
|
|
1071
|
+
createEvent: createSecurityEvent,
|
|
1072
|
+
createAlert: createSecurityAlert,
|
|
1073
|
+
filterEvents,
|
|
1074
|
+
calculateMetrics: calculateSecurityMetrics
|
|
1075
|
+
};
|
|
1076
|
+
|
|
1077
|
+
// src/SecurityService.ts
|
|
1078
|
+
import { NextResponse } from "next/server";
|
|
1079
|
+
import { createHmac as createHmac2 } from "crypto";
|
|
1080
|
+
async function getPayloadClient() {
|
|
1081
|
+
return { logger: console };
|
|
1082
|
+
}
|
|
1083
|
+
var logger = {
|
|
1084
|
+
info: (message, ...args) => console.log(`[SecurityService] ${message}`, ...args),
|
|
1085
|
+
warn: (message, ...args) => console.warn(`[SecurityService] ${message}`, ...args),
|
|
1086
|
+
error: (message, ...args) => console.error(`[SecurityService] ${message}`, ...args)
|
|
1087
|
+
};
|
|
1088
|
+
async function securityMiddleware(req) {
|
|
1089
|
+
const response = new NextResponse();
|
|
1090
|
+
try {
|
|
1091
|
+
const payload = await getPayloadClient();
|
|
1092
|
+
payload.logger.info(`Security middleware started for ${req.method} ${req.url}`);
|
|
1093
|
+
} catch {
|
|
1094
|
+
logger.info(`Security middleware started for ${req.method} ${req.url}`);
|
|
1095
|
+
}
|
|
1096
|
+
response.headers.set("X-Content-Type-Options", "nosniff");
|
|
1097
|
+
response.headers.set("X-Frame-Options", "DENY");
|
|
1098
|
+
response.headers.set("X-XSS-Protection", "1; mode=block");
|
|
1099
|
+
response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
1100
|
+
response.headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
|
1101
|
+
const isApiRoute = req.nextUrl.pathname.startsWith("/api");
|
|
1102
|
+
const csp = isApiRoute ? "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'" : "default-src 'self'";
|
|
1103
|
+
response.headers.set("Content-Security-Policy", csp);
|
|
1104
|
+
try {
|
|
1105
|
+
const payload = await getPayloadClient();
|
|
1106
|
+
payload.logger.info("Security middleware completed");
|
|
1107
|
+
} catch {
|
|
1108
|
+
logger.info("Security middleware completed");
|
|
1109
|
+
}
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
function createSecurity(config) {
|
|
1113
|
+
return {
|
|
1114
|
+
generateCsrfToken(sessionId) {
|
|
1115
|
+
return createHmac2("sha256", config.csrfSecret).update(sessionId).digest("hex");
|
|
1116
|
+
},
|
|
1117
|
+
isTokenExpired(exp) {
|
|
1118
|
+
return Date.now() / 1e3 > exp;
|
|
1119
|
+
},
|
|
1120
|
+
getAccessTokenExpiry() {
|
|
1121
|
+
return config.tokenExpiry.access;
|
|
1122
|
+
},
|
|
1123
|
+
getRefreshTokenExpiry() {
|
|
1124
|
+
return config.tokenExpiry.refresh;
|
|
1125
|
+
}
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// src/SessionService.ts
|
|
1130
|
+
var logger2 = {
|
|
1131
|
+
info: (message, ...args) => console.log(`[SessionService] ${message}`, ...args),
|
|
1132
|
+
warn: (message, ...args) => console.warn(`[SessionService] ${message}`, ...args),
|
|
1133
|
+
error: (message, ...args) => console.error(`[SessionService] ${message}`, ...args)
|
|
1134
|
+
};
|
|
1135
|
+
var devLogger3 = {
|
|
1136
|
+
info: (...args) => logger2.info(String(args[0]), ...args.slice(1)),
|
|
1137
|
+
warn: (...args) => logger2.warn(String(args[0]), ...args.slice(1)),
|
|
1138
|
+
error: (...args) => logger2.error(String(args[0]), ...args.slice(1)),
|
|
1139
|
+
forService: (_name) => ({
|
|
1140
|
+
info: (...args) => logger2.info(String(args[0]), ...args.slice(1)),
|
|
1141
|
+
warn: (...args) => logger2.warn(String(args[0]), ...args.slice(1)),
|
|
1142
|
+
error: (...args) => logger2.error(String(args[0]), ...args.slice(1))
|
|
1143
|
+
})
|
|
1144
|
+
};
|
|
1145
|
+
async function getPayloadClient2() {
|
|
1146
|
+
return {
|
|
1147
|
+
logger: {
|
|
1148
|
+
info: (...args) => logger2.info(String(args[0]), ...args.slice(1)),
|
|
1149
|
+
warn: (...args) => logger2.warn(String(args[0]), ...args.slice(1)),
|
|
1150
|
+
error: (...args) => logger2.error(String(args[0]), ...args.slice(1))
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
function generateUUID() {
|
|
1155
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
1156
|
+
return crypto.randomUUID();
|
|
1157
|
+
}
|
|
1158
|
+
return `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`.replace(/[xy]/g, (c) => {
|
|
1159
|
+
const r = Math.random() * 16 | 0;
|
|
1160
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
1161
|
+
return v.toString(16);
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
var SessionRepository = class {
|
|
1165
|
+
constructor(payload) {
|
|
1166
|
+
this.payload = payload;
|
|
1167
|
+
void this.payload;
|
|
1168
|
+
}
|
|
1169
|
+
sessions = /* @__PURE__ */ new Map();
|
|
1170
|
+
createSessionId() {
|
|
1171
|
+
try {
|
|
1172
|
+
return generateUUID();
|
|
1173
|
+
} catch {
|
|
1174
|
+
return `session_${Date.now().toString(36)}`;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
createToken() {
|
|
1178
|
+
try {
|
|
1179
|
+
return `token_${generateUUID()}`;
|
|
1180
|
+
} catch {
|
|
1181
|
+
return `token_${Date.now().toString(36)}`;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
async create(data) {
|
|
1185
|
+
if (!data.userId) {
|
|
1186
|
+
return {
|
|
1187
|
+
success: false,
|
|
1188
|
+
errors: [{ message: "userId is required", path: "create" }]
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
const now = /* @__PURE__ */ new Date();
|
|
1192
|
+
const session = {
|
|
1193
|
+
id: data.id ?? this.createSessionId(),
|
|
1194
|
+
userId: data.userId,
|
|
1195
|
+
token: data.token ?? this.createToken(),
|
|
1196
|
+
expiresAt: data.expiresAt ? new Date(data.expiresAt) : new Date(now.getTime() + 1e3 * 60 * 60),
|
|
1197
|
+
lastActiveAt: data.lastActiveAt ? new Date(data.lastActiveAt) : now,
|
|
1198
|
+
isActive: data.isActive ?? true,
|
|
1199
|
+
updatedAt: now,
|
|
1200
|
+
createdAt: now,
|
|
1201
|
+
...data.device !== void 0 ? { device: data.device } : {}
|
|
1202
|
+
};
|
|
1203
|
+
this.sessions.set(session.id, session);
|
|
1204
|
+
return { success: true, data: session };
|
|
1205
|
+
}
|
|
1206
|
+
async findById(id) {
|
|
1207
|
+
const session = this.sessions.get(String(id));
|
|
1208
|
+
if (!session) {
|
|
1209
|
+
return {
|
|
1210
|
+
success: false,
|
|
1211
|
+
errors: [{ message: "Session not found", path: "findById" }]
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
return { success: true, data: session };
|
|
1215
|
+
}
|
|
1216
|
+
async delete(id) {
|
|
1217
|
+
const key = String(id);
|
|
1218
|
+
if (!this.sessions.has(key)) {
|
|
1219
|
+
return {
|
|
1220
|
+
success: false,
|
|
1221
|
+
errors: [{ message: "Session not found", path: "delete" }]
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
this.sessions.delete(key);
|
|
1225
|
+
return { success: true };
|
|
1226
|
+
}
|
|
1227
|
+
async revokeAllUserSessions(userId) {
|
|
1228
|
+
let removed = false;
|
|
1229
|
+
for (const [key, session] of this.sessions.entries()) {
|
|
1230
|
+
if (session.userId === userId) {
|
|
1231
|
+
this.sessions.delete(key);
|
|
1232
|
+
removed = true;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
if (!removed) {
|
|
1236
|
+
return {
|
|
1237
|
+
success: false,
|
|
1238
|
+
errors: [{ message: "No sessions found for user", path: "revokeAllUserSessions" }]
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
return { success: true };
|
|
1242
|
+
}
|
|
1243
|
+
async findAll() {
|
|
1244
|
+
return { success: true, data: Array.from(this.sessions.values()) };
|
|
1245
|
+
}
|
|
1246
|
+
async update(id, data) {
|
|
1247
|
+
const existing = this.sessions.get(id);
|
|
1248
|
+
if (!existing) {
|
|
1249
|
+
return {
|
|
1250
|
+
success: false,
|
|
1251
|
+
errors: [{ message: "Session not found", path: "update" }]
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
const updated = {
|
|
1255
|
+
...existing,
|
|
1256
|
+
...data.userId !== void 0 ? { userId: data.userId } : {},
|
|
1257
|
+
...data.token !== void 0 ? { token: data.token } : {},
|
|
1258
|
+
...data.device !== void 0 ? { device: data.device } : {},
|
|
1259
|
+
...data.expiresAt !== void 0 ? { expiresAt: new Date(data.expiresAt) } : {},
|
|
1260
|
+
...data.lastActiveAt !== void 0 ? { lastActiveAt: new Date(data.lastActiveAt) } : {},
|
|
1261
|
+
...data.isActive !== void 0 ? { isActive: data.isActive } : {},
|
|
1262
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1263
|
+
};
|
|
1264
|
+
this.sessions.set(id, updated);
|
|
1265
|
+
return { success: true, data: updated };
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1268
|
+
var SessionService = class {
|
|
1269
|
+
repository = null;
|
|
1270
|
+
constructor(repository) {
|
|
1271
|
+
if (repository) {
|
|
1272
|
+
this.repository = repository;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
async getRepository() {
|
|
1276
|
+
if (!this.repository) {
|
|
1277
|
+
const payload = await getPayloadClient2();
|
|
1278
|
+
this.repository = new SessionRepository(payload);
|
|
1279
|
+
}
|
|
1280
|
+
return this.repository;
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Creates a new session for a user.
|
|
1284
|
+
*/
|
|
1285
|
+
async createSession(data) {
|
|
1286
|
+
if (!data.userId || typeof data.userId !== "string") {
|
|
1287
|
+
return {
|
|
1288
|
+
success: false,
|
|
1289
|
+
errors: [
|
|
1290
|
+
{
|
|
1291
|
+
message: "userId is required",
|
|
1292
|
+
path: "userId"
|
|
1293
|
+
}
|
|
1294
|
+
]
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
const repository = await this.getRepository();
|
|
1298
|
+
const result = await repository.create(data);
|
|
1299
|
+
if (!result.success) {
|
|
1300
|
+
try {
|
|
1301
|
+
const payload = await getPayloadClient2();
|
|
1302
|
+
payload.logger.error("Failed to create session", {
|
|
1303
|
+
errors: result.errors
|
|
1304
|
+
});
|
|
1305
|
+
} catch {
|
|
1306
|
+
devLogger3.error("Failed to create session", { errors: result.errors });
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
return result;
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Validates a session by ID, checks for expiration, and deletes if expired.
|
|
1313
|
+
*/
|
|
1314
|
+
async validateSession(sessionId) {
|
|
1315
|
+
try {
|
|
1316
|
+
const repository = await this.getRepository();
|
|
1317
|
+
const result = await repository.findById(sessionId);
|
|
1318
|
+
if (!result.success || !result.data) {
|
|
1319
|
+
try {
|
|
1320
|
+
const payload = await getPayloadClient2();
|
|
1321
|
+
payload.logger.warn("Invalid session", { sessionId, errors: result.errors });
|
|
1322
|
+
} catch {
|
|
1323
|
+
devLogger3.warn("Invalid session", { sessionId, errors: result.errors });
|
|
1324
|
+
}
|
|
1325
|
+
return {
|
|
1326
|
+
success: false,
|
|
1327
|
+
errors: result.errors ?? [{ message: "Session not found", path: "validateSession" }]
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
const session = result.data;
|
|
1331
|
+
if (session.expiresAt.getTime() < Date.now()) {
|
|
1332
|
+
await repository.delete(sessionId);
|
|
1333
|
+
try {
|
|
1334
|
+
const payload = await getPayloadClient2();
|
|
1335
|
+
payload.logger.info("Session expired and deleted", { sessionId });
|
|
1336
|
+
} catch {
|
|
1337
|
+
devLogger3.info("Session expired and deleted", { sessionId });
|
|
1338
|
+
}
|
|
1339
|
+
return {
|
|
1340
|
+
success: false,
|
|
1341
|
+
errors: [{ message: "Session expired", path: "validateSession" }]
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
const userResult = { success: true, data: { id: session.userId } };
|
|
1345
|
+
if (!userResult.success || !userResult.data) {
|
|
1346
|
+
try {
|
|
1347
|
+
const payload = await getPayloadClient2();
|
|
1348
|
+
payload.logger.error("User not found for session", {
|
|
1349
|
+
sessionId,
|
|
1350
|
+
userId: session.userId
|
|
1351
|
+
});
|
|
1352
|
+
} catch {
|
|
1353
|
+
devLogger3.error("User not found for session", {
|
|
1354
|
+
sessionId,
|
|
1355
|
+
userId: session.userId
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
return {
|
|
1359
|
+
success: false,
|
|
1360
|
+
errors: [{ message: "User not found for session", path: "validateSession" }]
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
return { success: true, data: session };
|
|
1364
|
+
} catch (error) {
|
|
1365
|
+
try {
|
|
1366
|
+
const payload = await getPayloadClient2();
|
|
1367
|
+
payload.logger.error("Session validation failed", { error });
|
|
1368
|
+
} catch {
|
|
1369
|
+
devLogger3.error("Session validation failed", { error });
|
|
1370
|
+
}
|
|
1371
|
+
return {
|
|
1372
|
+
success: false,
|
|
1373
|
+
errors: [{ message: "Session validation failed", path: "validateSession" }]
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* Invalidates (deletes) a session by ID.
|
|
1379
|
+
*/
|
|
1380
|
+
async invalidateSession(sessionId) {
|
|
1381
|
+
try {
|
|
1382
|
+
const repository = await this.getRepository();
|
|
1383
|
+
const result = await repository.delete(sessionId);
|
|
1384
|
+
if (!result.success) {
|
|
1385
|
+
try {
|
|
1386
|
+
const payload = await getPayloadClient2();
|
|
1387
|
+
payload.logger.error("Session invalidation failed", {
|
|
1388
|
+
errors: result.errors
|
|
1389
|
+
});
|
|
1390
|
+
} catch {
|
|
1391
|
+
devLogger3.error("Session invalidation failed", {
|
|
1392
|
+
errors: result.errors
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
return result;
|
|
1396
|
+
}
|
|
1397
|
+
return result;
|
|
1398
|
+
} catch (error) {
|
|
1399
|
+
try {
|
|
1400
|
+
const payload = await getPayloadClient2();
|
|
1401
|
+
payload.logger.error("Session invalidation failed", { error });
|
|
1402
|
+
} catch {
|
|
1403
|
+
devLogger3.error("Session invalidation failed", { error });
|
|
1404
|
+
}
|
|
1405
|
+
return {
|
|
1406
|
+
success: false,
|
|
1407
|
+
errors: [
|
|
1408
|
+
{
|
|
1409
|
+
message: "Session invalidation failed",
|
|
1410
|
+
path: "invalidateSession"
|
|
1411
|
+
}
|
|
1412
|
+
]
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Invalidates (deletes) all sessions for a user by userId.
|
|
1418
|
+
*/
|
|
1419
|
+
async invalidateAllUserSessions(userId) {
|
|
1420
|
+
try {
|
|
1421
|
+
const repository = await this.getRepository();
|
|
1422
|
+
const result = await repository.revokeAllUserSessions(userId);
|
|
1423
|
+
if (!result.success) {
|
|
1424
|
+
try {
|
|
1425
|
+
const payload = await getPayloadClient2();
|
|
1426
|
+
payload.logger.error("User sessions invalidation failed", {
|
|
1427
|
+
errors: result.errors
|
|
1428
|
+
});
|
|
1429
|
+
} catch {
|
|
1430
|
+
devLogger3.error("User sessions invalidation failed", {
|
|
1431
|
+
errors: result.errors
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
return result;
|
|
1435
|
+
}
|
|
1436
|
+
return result;
|
|
1437
|
+
} catch (error) {
|
|
1438
|
+
try {
|
|
1439
|
+
const payload = await getPayloadClient2();
|
|
1440
|
+
payload.logger.error("User sessions invalidation failed", {
|
|
1441
|
+
error
|
|
1442
|
+
});
|
|
1443
|
+
} catch {
|
|
1444
|
+
devLogger3.error("User sessions invalidation failed", { error });
|
|
1445
|
+
}
|
|
1446
|
+
return {
|
|
1447
|
+
success: false,
|
|
1448
|
+
errors: [
|
|
1449
|
+
{
|
|
1450
|
+
message: "Failed to invalidate user sessions",
|
|
1451
|
+
path: "invalidateAllUserSessions"
|
|
1452
|
+
}
|
|
1453
|
+
]
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
async getSessionById(id) {
|
|
1458
|
+
const repository = await this.getRepository();
|
|
1459
|
+
const result = await repository.findById(id);
|
|
1460
|
+
if (!result.success) {
|
|
1461
|
+
try {
|
|
1462
|
+
const payload = await getPayloadClient2();
|
|
1463
|
+
payload.logger.warn("Session not found", {
|
|
1464
|
+
errors: result.errors
|
|
1465
|
+
});
|
|
1466
|
+
} catch {
|
|
1467
|
+
devLogger3.warn("Session not found", { errors: result.errors });
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
return result;
|
|
1471
|
+
}
|
|
1472
|
+
async getAllSessions() {
|
|
1473
|
+
const repository = await this.getRepository();
|
|
1474
|
+
const result = await repository.findAll();
|
|
1475
|
+
if (!result.success) {
|
|
1476
|
+
try {
|
|
1477
|
+
const payload = await getPayloadClient2();
|
|
1478
|
+
payload.logger.error("Failed to fetch sessions", {
|
|
1479
|
+
errors: result.errors
|
|
1480
|
+
});
|
|
1481
|
+
} catch {
|
|
1482
|
+
devLogger3.error("Failed to fetch sessions", {
|
|
1483
|
+
errors: result.errors
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
return result;
|
|
1488
|
+
}
|
|
1489
|
+
async updateSession(id, data) {
|
|
1490
|
+
const repository = await this.getRepository();
|
|
1491
|
+
const result = await repository.update(id, data);
|
|
1492
|
+
if (!result.success) {
|
|
1493
|
+
try {
|
|
1494
|
+
const payload = await getPayloadClient2();
|
|
1495
|
+
payload.logger.error("Failed to update session", {
|
|
1496
|
+
errors: result.errors
|
|
1497
|
+
});
|
|
1498
|
+
} catch {
|
|
1499
|
+
devLogger3.error("Failed to update session", { errors: result.errors });
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
return result;
|
|
1503
|
+
}
|
|
1504
|
+
async deleteSession(id) {
|
|
1505
|
+
const repository = await this.getRepository();
|
|
1506
|
+
const result = await repository.delete(id);
|
|
1507
|
+
if (!result.success) {
|
|
1508
|
+
try {
|
|
1509
|
+
const payload = await getPayloadClient2();
|
|
1510
|
+
payload.logger.error("Failed to delete session", {
|
|
1511
|
+
errors: result.errors
|
|
1512
|
+
});
|
|
1513
|
+
} catch {
|
|
1514
|
+
devLogger3.error("Failed to delete session", { errors: result.errors });
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
return result;
|
|
1518
|
+
}
|
|
1519
|
+
};
|
|
1520
|
+
var sessionService = new SessionService();
|
|
1521
|
+
|
|
1522
|
+
// src/TokenService.ts
|
|
1523
|
+
import { InfrastructureService as InfrastructureService2 } from "@revealui/core";
|
|
1524
|
+
var AppError = class extends Error {
|
|
1525
|
+
constructor(code, message, cause) {
|
|
1526
|
+
super(message);
|
|
1527
|
+
this.code = code;
|
|
1528
|
+
this.cause = cause;
|
|
1529
|
+
this.name = "AppError";
|
|
1530
|
+
if (cause) {
|
|
1531
|
+
this.cause = cause;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
};
|
|
1535
|
+
var ErrorCode = {
|
|
1536
|
+
TOKEN_INVALID: "TOKEN_INVALID",
|
|
1537
|
+
TOKEN_EXPIRED: "TOKEN_EXPIRED",
|
|
1538
|
+
AUTHENTICATION_ERROR: "AUTHENTICATION_ERROR",
|
|
1539
|
+
INTERNAL_ERROR: "INTERNAL_ERROR"
|
|
1540
|
+
};
|
|
1541
|
+
var TokenService = class extends InfrastructureService2 {
|
|
1542
|
+
constructor(payload) {
|
|
1543
|
+
super(payload, "TokenService");
|
|
1544
|
+
this.payload = payload;
|
|
1545
|
+
}
|
|
1546
|
+
/**
|
|
1547
|
+
* Authenticate user and generate access token using PayloadCMS native login
|
|
1548
|
+
* @param credentials User login credentials
|
|
1549
|
+
* @param req Optional PayloadRequest for context
|
|
1550
|
+
* @returns Login result with user data and token
|
|
1551
|
+
*/
|
|
1552
|
+
async login(credentials, req) {
|
|
1553
|
+
try {
|
|
1554
|
+
this.logger.debug("Attempting user login", {
|
|
1555
|
+
email: credentials.email,
|
|
1556
|
+
serviceName: this.serviceName,
|
|
1557
|
+
operation: "login"
|
|
1558
|
+
});
|
|
1559
|
+
const result = await this.payload.login({
|
|
1560
|
+
collection: "users",
|
|
1561
|
+
data: {
|
|
1562
|
+
email: credentials.email,
|
|
1563
|
+
password: credentials.password
|
|
1564
|
+
},
|
|
1565
|
+
...req && { req }
|
|
1566
|
+
});
|
|
1567
|
+
this.logger.info("User login successful", {
|
|
1568
|
+
userId: result.user.id,
|
|
1569
|
+
email: result.user.email,
|
|
1570
|
+
serviceName: this.serviceName
|
|
1571
|
+
});
|
|
1572
|
+
return result;
|
|
1573
|
+
} catch (error) {
|
|
1574
|
+
this.logger.error("User login failed", {
|
|
1575
|
+
email: credentials.email,
|
|
1576
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
1577
|
+
serviceName: this.serviceName
|
|
1578
|
+
});
|
|
1579
|
+
throw new AppError(
|
|
1580
|
+
ErrorCode.AUTHENTICATION_ERROR,
|
|
1581
|
+
"Login failed",
|
|
1582
|
+
error instanceof Error ? error : new Error(String(error))
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
/**
|
|
1587
|
+
* Verify user by ID using PayloadCMS native functionality
|
|
1588
|
+
* @param userId User ID to verify
|
|
1589
|
+
* @returns User data if exists
|
|
1590
|
+
*/
|
|
1591
|
+
async verifyUser(userId) {
|
|
1592
|
+
try {
|
|
1593
|
+
this.logger.debug("Verifying user", {
|
|
1594
|
+
userId,
|
|
1595
|
+
serviceName: this.serviceName,
|
|
1596
|
+
operation: "verifyUser"
|
|
1597
|
+
});
|
|
1598
|
+
const user = await this.payload.findByID({
|
|
1599
|
+
collection: "users",
|
|
1600
|
+
id: userId
|
|
1601
|
+
});
|
|
1602
|
+
this.logger.debug("User verification successful", {
|
|
1603
|
+
userId: user.id,
|
|
1604
|
+
serviceName: this.serviceName
|
|
1605
|
+
});
|
|
1606
|
+
return user;
|
|
1607
|
+
} catch (error) {
|
|
1608
|
+
this.logger.error("User verification failed", {
|
|
1609
|
+
userId,
|
|
1610
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
1611
|
+
serviceName: this.serviceName
|
|
1612
|
+
});
|
|
1613
|
+
throw new AppError(
|
|
1614
|
+
ErrorCode.AUTHENTICATION_ERROR,
|
|
1615
|
+
"User verification failed",
|
|
1616
|
+
error instanceof Error ? error : new Error(String(error))
|
|
1617
|
+
);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Generate password reset token using PayloadCMS native functionality
|
|
1622
|
+
* @param email User email address
|
|
1623
|
+
* @returns Password reset token
|
|
1624
|
+
*/
|
|
1625
|
+
async generatePasswordResetToken(email) {
|
|
1626
|
+
try {
|
|
1627
|
+
this.logger.debug("Generating password reset token", {
|
|
1628
|
+
email,
|
|
1629
|
+
serviceName: this.serviceName,
|
|
1630
|
+
operation: "generatePasswordResetToken"
|
|
1631
|
+
});
|
|
1632
|
+
const token = await this.payload.forgotPassword({
|
|
1633
|
+
collection: "users",
|
|
1634
|
+
data: { email }
|
|
1635
|
+
});
|
|
1636
|
+
this.logger.info("Password reset token generated", {
|
|
1637
|
+
email,
|
|
1638
|
+
serviceName: this.serviceName
|
|
1639
|
+
});
|
|
1640
|
+
return token;
|
|
1641
|
+
} catch (error) {
|
|
1642
|
+
this.logger.error("Password reset token generation failed", {
|
|
1643
|
+
email,
|
|
1644
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
1645
|
+
serviceName: this.serviceName
|
|
1646
|
+
});
|
|
1647
|
+
throw new AppError(
|
|
1648
|
+
ErrorCode.INTERNAL_ERROR,
|
|
1649
|
+
"Failed to generate password reset token",
|
|
1650
|
+
error instanceof Error ? error : new Error(String(error))
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
/**
|
|
1655
|
+
* Reset user password using PayloadCMS native functionality
|
|
1656
|
+
* @param token Password reset token
|
|
1657
|
+
* @param newPassword New password
|
|
1658
|
+
* @returns Reset result with user and new token
|
|
1659
|
+
*
|
|
1660
|
+
* NOTE: Uses overrideAccess: true as this is a system-level password reset operation
|
|
1661
|
+
* that requires bypassing normal access control for security token validation.
|
|
1662
|
+
*/
|
|
1663
|
+
async resetPassword(token, newPassword) {
|
|
1664
|
+
try {
|
|
1665
|
+
this.logger.debug("Resetting password", {
|
|
1666
|
+
serviceName: this.serviceName,
|
|
1667
|
+
operation: "resetPassword"
|
|
1668
|
+
});
|
|
1669
|
+
const result = await this.payload.resetPassword({
|
|
1670
|
+
collection: "users",
|
|
1671
|
+
data: {
|
|
1672
|
+
token,
|
|
1673
|
+
password: newPassword
|
|
1674
|
+
},
|
|
1675
|
+
overrideAccess: true
|
|
1676
|
+
// System-level operation for password reset flow
|
|
1677
|
+
});
|
|
1678
|
+
this.logger.info("Password reset successful", {
|
|
1679
|
+
userId: result.user?.id,
|
|
1680
|
+
serviceName: this.serviceName
|
|
1681
|
+
});
|
|
1682
|
+
return result;
|
|
1683
|
+
} catch (error) {
|
|
1684
|
+
this.logger.error("Password reset failed", {
|
|
1685
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
1686
|
+
serviceName: this.serviceName
|
|
1687
|
+
});
|
|
1688
|
+
throw new AppError(
|
|
1689
|
+
ErrorCode.AUTHENTICATION_ERROR,
|
|
1690
|
+
"Password reset failed",
|
|
1691
|
+
error instanceof Error ? error : new Error(String(error))
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
/**
|
|
1696
|
+
* Verify email using PayloadCMS native functionality
|
|
1697
|
+
* @param token Email verification token
|
|
1698
|
+
* @returns Verification success status
|
|
1699
|
+
*/
|
|
1700
|
+
async verifyEmail(token) {
|
|
1701
|
+
try {
|
|
1702
|
+
this.logger.debug("Verifying email", {
|
|
1703
|
+
serviceName: this.serviceName,
|
|
1704
|
+
operation: "verifyEmail"
|
|
1705
|
+
});
|
|
1706
|
+
const result = await this.payload.verifyEmail({
|
|
1707
|
+
collection: "users",
|
|
1708
|
+
token
|
|
1709
|
+
});
|
|
1710
|
+
this.logger.info("Email verification completed", {
|
|
1711
|
+
success: result,
|
|
1712
|
+
serviceName: this.serviceName
|
|
1713
|
+
});
|
|
1714
|
+
return result;
|
|
1715
|
+
} catch (error) {
|
|
1716
|
+
this.logger.error("Email verification failed", {
|
|
1717
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
1718
|
+
serviceName: this.serviceName
|
|
1719
|
+
});
|
|
1720
|
+
throw new AppError(
|
|
1721
|
+
ErrorCode.AUTHENTICATION_ERROR,
|
|
1722
|
+
"Email verification failed",
|
|
1723
|
+
error instanceof Error ? error : new Error(String(error))
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
/**
|
|
1728
|
+
* Unlock user account using PayloadCMS native functionality
|
|
1729
|
+
* @param email User email address
|
|
1730
|
+
* @param password User password
|
|
1731
|
+
* @returns Unlock success status
|
|
1732
|
+
*
|
|
1733
|
+
* NOTE: Uses overrideAccess: true as this is a system-level account unlock operation
|
|
1734
|
+
* that requires bypassing normal access control for account recovery.
|
|
1735
|
+
*/
|
|
1736
|
+
async unlockUser(email, password) {
|
|
1737
|
+
try {
|
|
1738
|
+
this.logger.debug("Unlocking user account", {
|
|
1739
|
+
email,
|
|
1740
|
+
serviceName: this.serviceName,
|
|
1741
|
+
operation: "unlockUser"
|
|
1742
|
+
});
|
|
1743
|
+
const result = await this.payload.unlock({
|
|
1744
|
+
collection: "users",
|
|
1745
|
+
data: { email, password },
|
|
1746
|
+
overrideAccess: true
|
|
1747
|
+
// System-level operation for account recovery
|
|
1748
|
+
});
|
|
1749
|
+
this.logger.info("User account unlock completed", {
|
|
1750
|
+
email,
|
|
1751
|
+
success: result,
|
|
1752
|
+
serviceName: this.serviceName
|
|
1753
|
+
});
|
|
1754
|
+
return result;
|
|
1755
|
+
} catch (error) {
|
|
1756
|
+
this.logger.error("User account unlock failed", {
|
|
1757
|
+
email,
|
|
1758
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
1759
|
+
serviceName: this.serviceName
|
|
1760
|
+
});
|
|
1761
|
+
throw new AppError(
|
|
1762
|
+
ErrorCode.AUTHENTICATION_ERROR,
|
|
1763
|
+
"User account unlock failed",
|
|
1764
|
+
error instanceof Error ? error : new Error(String(error))
|
|
1765
|
+
);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1769
|
+
* @inheritdoc
|
|
1770
|
+
*/
|
|
1771
|
+
onInitialize() {
|
|
1772
|
+
this.logger.info("TokenService initialized with PayloadCMS native auth", {
|
|
1773
|
+
serviceName: this.serviceName
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
/**
|
|
1777
|
+
* @inheritdoc
|
|
1778
|
+
*/
|
|
1779
|
+
onCleanup() {
|
|
1780
|
+
this.logger.info("TokenService cleaned up", {
|
|
1781
|
+
serviceName: this.serviceName
|
|
1782
|
+
});
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1785
|
+
* @inheritdoc
|
|
1786
|
+
*/
|
|
1787
|
+
onHealthCheck() {
|
|
1788
|
+
this.logger.debug("TokenService health check passed", {
|
|
1789
|
+
serviceName: this.serviceName
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
};
|
|
1793
|
+
function createTokenService(payload) {
|
|
1794
|
+
return new TokenService(payload);
|
|
1795
|
+
}
|
|
1796
|
+
export {
|
|
1797
|
+
CSRFService,
|
|
1798
|
+
EncryptionService,
|
|
1799
|
+
RATE_LIMITS,
|
|
1800
|
+
RateLimited,
|
|
1801
|
+
RateLimiterService,
|
|
1802
|
+
SECURITY_HEADERS,
|
|
1803
|
+
SecurityMonitoringService,
|
|
1804
|
+
SessionService,
|
|
1805
|
+
TokenService,
|
|
1806
|
+
addSecurityEvent,
|
|
1807
|
+
calculateSecurityMetrics,
|
|
1808
|
+
checkAnonymousRateLimit,
|
|
1809
|
+
checkRateLimit,
|
|
1810
|
+
checkUserRateLimit,
|
|
1811
|
+
clearSecurityData,
|
|
1812
|
+
createSecurity,
|
|
1813
|
+
createSecurityAlert,
|
|
1814
|
+
createSecurityEvent,
|
|
1815
|
+
createTokenService,
|
|
1816
|
+
filterEvents,
|
|
1817
|
+
generateIPRateLimitKey,
|
|
1818
|
+
generateRateLimitKey,
|
|
1819
|
+
getClientIP,
|
|
1820
|
+
getEventsBySeverity,
|
|
1821
|
+
getEventsInTimeRange,
|
|
1822
|
+
getRateLimiterService,
|
|
1823
|
+
getSecurityAlerts,
|
|
1824
|
+
getSecurityEvents,
|
|
1825
|
+
getSecurityMetrics,
|
|
1826
|
+
getUserEvents,
|
|
1827
|
+
isTrustedSource,
|
|
1828
|
+
logAuthenticationEvent,
|
|
1829
|
+
logAuthorizationEvent,
|
|
1830
|
+
logDataAccessEvent,
|
|
1831
|
+
logSecurityEvent,
|
|
1832
|
+
sanitizeInput,
|
|
1833
|
+
securityMiddleware,
|
|
1834
|
+
sessionService,
|
|
1835
|
+
shouldTriggerAlert,
|
|
1836
|
+
validateAndSanitizeInput,
|
|
1837
|
+
validateCSRFToken,
|
|
1838
|
+
validateEnvironment,
|
|
1839
|
+
withRateLimit
|
|
1840
|
+
};
|
|
1841
|
+
//# sourceMappingURL=index.js.map
|