@voltrix/security 0.3.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 +21 -0
- package/README.md +147 -0
- package/dist/decorators/index.d.ts +26 -0
- package/dist/decorators/index.js +37 -0
- package/dist/decorators/index.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +86 -0
- package/dist/index.js.map +1 -0
- package/dist/modules/cors.d.ts +8 -0
- package/dist/modules/cors.js +71 -0
- package/dist/modules/cors.js.map +1 -0
- package/dist/modules/csrf.d.ts +20 -0
- package/dist/modules/csrf.js +98 -0
- package/dist/modules/csrf.js.map +1 -0
- package/dist/modules/helmet.d.ts +7 -0
- package/dist/modules/helmet.js +65 -0
- package/dist/modules/helmet.js.map +1 -0
- package/dist/modules/ip-filter.d.ts +21 -0
- package/dist/modules/ip-filter.js +99 -0
- package/dist/modules/ip-filter.js.map +1 -0
- package/dist/modules/rate-limit.d.ts +11 -0
- package/dist/modules/rate-limit.js +58 -0
- package/dist/modules/rate-limit.js.map +1 -0
- package/dist/modules/session.d.ts +16 -0
- package/dist/modules/session.js +157 -0
- package/dist/modules/session.js.map +1 -0
- package/dist/store/index.d.ts +3 -0
- package/dist/store/index.js +3 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/memory-store.d.ts +23 -0
- package/dist/store/memory-store.js +75 -0
- package/dist/store/memory-store.js.map +1 -0
- package/dist/store/redis-store.d.ts +20 -0
- package/dist/store/redis-store.js +55 -0
- package/dist/store/redis-store.js.map +1 -0
- package/dist/types/index.d.ts +98 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { IpFilterOptions } from '../types/index.js';
|
|
2
|
+
import type { Middleware } from '@voltrix/core';
|
|
3
|
+
/**
|
|
4
|
+
* Compiled IP and CIDR matching engine.
|
|
5
|
+
* Converts IPv4 CIDR blocks into 32-bit binary integer boundaries for nanosecond-level bitwise matching.
|
|
6
|
+
*/
|
|
7
|
+
export declare class IpMatcher {
|
|
8
|
+
private readonly _v4Ranges;
|
|
9
|
+
private readonly _v6Ranges;
|
|
10
|
+
constructor(cidrs: string[]);
|
|
11
|
+
private ipToLong;
|
|
12
|
+
/**
|
|
13
|
+
* Matches an IP address against compiled ranges.
|
|
14
|
+
*/
|
|
15
|
+
match(ip: string): boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Hyper-performance IP Filtering Middleware.
|
|
19
|
+
* Rejects requests from unauthorized IPs or subnets in the earliest onRequest hook.
|
|
20
|
+
*/
|
|
21
|
+
export declare function ipFilter(options?: IpFilterOptions | boolean): Middleware;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { getClientIp } from './rate-limit.js';
|
|
2
|
+
/**
|
|
3
|
+
* Compiled IP and CIDR matching engine.
|
|
4
|
+
* Converts IPv4 CIDR blocks into 32-bit binary integer boundaries for nanosecond-level bitwise matching.
|
|
5
|
+
*/
|
|
6
|
+
export class IpMatcher {
|
|
7
|
+
_v4Ranges = [];
|
|
8
|
+
_v6Ranges = [];
|
|
9
|
+
constructor(cidrs) {
|
|
10
|
+
for (const cidr of cidrs) {
|
|
11
|
+
const trimmed = cidr.trim();
|
|
12
|
+
if (trimmed.includes(':')) {
|
|
13
|
+
// Simple exact matching for IPv6
|
|
14
|
+
this._v6Ranges.push(trimmed);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
// IPv4 Bitwise compile
|
|
18
|
+
const parts = trimmed.split('/');
|
|
19
|
+
const ip = parts[0].trim();
|
|
20
|
+
const maskBits = parts[1] ? parseInt(parts[1], 10) : 32;
|
|
21
|
+
try {
|
|
22
|
+
const base = this.ipToLong(ip);
|
|
23
|
+
const mask = maskBits === 0 ? 0 : (~0 << (32 - maskBits)) >>> 0;
|
|
24
|
+
this._v4Ranges.push({ base: (base & mask) >>> 0, mask });
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Ignore invalid IP strings during parsing
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
ipToLong(ip) {
|
|
33
|
+
const parts = ip.split('.');
|
|
34
|
+
if (parts.length !== 4)
|
|
35
|
+
throw new Error('Invalid IPv4');
|
|
36
|
+
return ((parseInt(parts[0], 10) << 24) >>> 0) +
|
|
37
|
+
((parseInt(parts[1], 10) << 16) >>> 0) +
|
|
38
|
+
((parseInt(parts[2], 10) << 8) >>> 0) +
|
|
39
|
+
(parseInt(parts[3], 10) >>> 0);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Matches an IP address against compiled ranges.
|
|
43
|
+
*/
|
|
44
|
+
match(ip) {
|
|
45
|
+
const cleanIp = ip.trim();
|
|
46
|
+
if (cleanIp.includes(':')) {
|
|
47
|
+
return this._v6Ranges.includes(cleanIp);
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const ipLong = this.ipToLong(cleanIp);
|
|
51
|
+
const ranges = this._v4Ranges;
|
|
52
|
+
for (let i = 0; i < ranges.length; i++) {
|
|
53
|
+
const r = ranges[i];
|
|
54
|
+
if ((ipLong & r.mask) === r.base) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch { }
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Hyper-performance IP Filtering Middleware.
|
|
65
|
+
* Rejects requests from unauthorized IPs or subnets in the earliest onRequest hook.
|
|
66
|
+
*/
|
|
67
|
+
export function ipFilter(options) {
|
|
68
|
+
if (options === false || !options) {
|
|
69
|
+
return (req, res, next) => next();
|
|
70
|
+
}
|
|
71
|
+
const opts = typeof options === 'object' ? options : {};
|
|
72
|
+
const whitelistMatcher = opts.whitelist ? new IpMatcher(opts.whitelist) : null;
|
|
73
|
+
const blacklistMatcher = opts.blacklist ? new IpMatcher(opts.blacklist) : null;
|
|
74
|
+
const handler = opts.handler ?? ((req, res) => {
|
|
75
|
+
res.status(403).end('Forbidden');
|
|
76
|
+
});
|
|
77
|
+
return async (req, res, next) => {
|
|
78
|
+
const ip = getClientIp(req);
|
|
79
|
+
let allowed = true;
|
|
80
|
+
// 1. Whitelist logic: IP MUST match whitelist if whitelist is defined
|
|
81
|
+
if (whitelistMatcher) {
|
|
82
|
+
allowed = whitelistMatcher.match(ip);
|
|
83
|
+
}
|
|
84
|
+
// 2. Blacklist logic: IP MUST NOT match blacklist if blacklist is defined
|
|
85
|
+
else if (blacklistMatcher) {
|
|
86
|
+
allowed = !blacklistMatcher.match(ip);
|
|
87
|
+
}
|
|
88
|
+
if (!allowed) {
|
|
89
|
+
// Abort pipeline immediately
|
|
90
|
+
const result = handler(req, res);
|
|
91
|
+
if (result instanceof Promise) {
|
|
92
|
+
await result;
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
next();
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=ip-filter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ip-filter.js","sourceRoot":"","sources":["../../src/modules/ip-filter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RateLimitOptions } from '../types/index.js';
|
|
2
|
+
import type { IRequest, Middleware } from '@voltrix/core';
|
|
3
|
+
/**
|
|
4
|
+
* Standard utility to resolve the client IP address from proxy-aware HTTP headers.
|
|
5
|
+
*/
|
|
6
|
+
export declare function getClientIp(req: IRequest): string;
|
|
7
|
+
/**
|
|
8
|
+
* Hyper-performance Rate Limiter Middleware.
|
|
9
|
+
* Restricts request frequency per IP or custom key dynamically.
|
|
10
|
+
*/
|
|
11
|
+
export declare function rateLimit(options?: RateLimitOptions | boolean): Middleware;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { MemoryStore } from '../store/memory-store.js';
|
|
2
|
+
/**
|
|
3
|
+
* Standard utility to resolve the client IP address from proxy-aware HTTP headers.
|
|
4
|
+
*/
|
|
5
|
+
export function getClientIp(req) {
|
|
6
|
+
const xForwardedFor = req.header('x-forwarded-for');
|
|
7
|
+
if (xForwardedFor) {
|
|
8
|
+
const first = xForwardedFor.split(',')[0].trim();
|
|
9
|
+
if (first)
|
|
10
|
+
return first;
|
|
11
|
+
}
|
|
12
|
+
return req.header('x-real-ip') || req.header('cf-connecting-ip') || '127.0.0.1';
|
|
13
|
+
}
|
|
14
|
+
// Share a single global MemoryStore instance by default
|
|
15
|
+
const defaultStore = new MemoryStore();
|
|
16
|
+
/**
|
|
17
|
+
* Hyper-performance Rate Limiter Middleware.
|
|
18
|
+
* Restricts request frequency per IP or custom key dynamically.
|
|
19
|
+
*/
|
|
20
|
+
export function rateLimit(options) {
|
|
21
|
+
if (options === false) {
|
|
22
|
+
return (req, res, next) => next();
|
|
23
|
+
}
|
|
24
|
+
const opts = typeof options === 'object' ? options : {};
|
|
25
|
+
const windowMs = opts.windowMs ?? 60000;
|
|
26
|
+
const limit = opts.limit ?? 100;
|
|
27
|
+
const store = opts.store ?? defaultStore;
|
|
28
|
+
const keyGenerator = opts.keyGenerator ?? getClientIp;
|
|
29
|
+
const handler = opts.handler ?? ((req, res) => {
|
|
30
|
+
res.status(429).json({ error: 'Too Many Requests' });
|
|
31
|
+
});
|
|
32
|
+
return async (req, res, next) => {
|
|
33
|
+
try {
|
|
34
|
+
const idKey = await keyGenerator(req);
|
|
35
|
+
const storeKey = `voltrix:security:rate-limit:${idKey}`;
|
|
36
|
+
const current = await store.increment(storeKey, windowMs);
|
|
37
|
+
const remaining = Math.max(0, limit - current);
|
|
38
|
+
const resetTimeSec = Math.ceil((Date.now() + windowMs) / 1000);
|
|
39
|
+
// Append standard Rate Limit headers
|
|
40
|
+
res.setHeader('RateLimit-Limit', String(limit));
|
|
41
|
+
res.setHeader('RateLimit-Remaining', String(remaining));
|
|
42
|
+
res.setHeader('RateLimit-Reset', String(resetTimeSec));
|
|
43
|
+
if (current > limit) {
|
|
44
|
+
// Exceeded limit: invoke handler and abort pipeline
|
|
45
|
+
const result = handler(req, res);
|
|
46
|
+
if (result instanceof Promise) {
|
|
47
|
+
await result;
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
next();
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
next(err);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=rate-limit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../../src/modules/rate-limit.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { SessionOptions } from '../types/index.js';
|
|
2
|
+
import type { Middleware } from '@voltrix/core';
|
|
3
|
+
/**
|
|
4
|
+
* AES-256-GCM Authenticated Encryption helper.
|
|
5
|
+
*/
|
|
6
|
+
export declare function encrypt(plainText: string, key: Buffer): string;
|
|
7
|
+
/**
|
|
8
|
+
* AES-256-GCM Authenticated Decryption helper.
|
|
9
|
+
* Strictly checks integrity tags to prevent tamper/oracle attacks.
|
|
10
|
+
*/
|
|
11
|
+
export declare function decrypt(cipherText: string, key: Buffer): string | null;
|
|
12
|
+
/**
|
|
13
|
+
* High-performance Secure Session Middleware.
|
|
14
|
+
* Supports stateless encrypted cookies (AES-256-GCM) and stateful shared session stores.
|
|
15
|
+
*/
|
|
16
|
+
export declare function session(options: SessionOptions): Middleware;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { createHash, randomBytes, createCipheriv, createDecipheriv } from 'node:crypto';
|
|
2
|
+
import { parseCookies, serializeCookie } from './csrf.js';
|
|
3
|
+
/**
|
|
4
|
+
* Derives a robust 32-byte cryptographic key from any string using SHA-256.
|
|
5
|
+
*/
|
|
6
|
+
function deriveKey(secret) {
|
|
7
|
+
return createHash('sha256').update(secret).digest();
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* AES-256-GCM Authenticated Encryption helper.
|
|
11
|
+
*/
|
|
12
|
+
export function encrypt(plainText, key) {
|
|
13
|
+
const iv = randomBytes(12);
|
|
14
|
+
const cipher = createCipheriv('aes-256-gcm', key, iv);
|
|
15
|
+
let encrypted = cipher.update(plainText, 'utf8', 'hex');
|
|
16
|
+
encrypted += cipher.final('hex');
|
|
17
|
+
const tag = cipher.getAuthTag();
|
|
18
|
+
return `${iv.toString('hex')}:${encrypted}:${tag.toString('hex')}`;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* AES-256-GCM Authenticated Decryption helper.
|
|
22
|
+
* Strictly checks integrity tags to prevent tamper/oracle attacks.
|
|
23
|
+
*/
|
|
24
|
+
export function decrypt(cipherText, key) {
|
|
25
|
+
try {
|
|
26
|
+
const parts = cipherText.split(':');
|
|
27
|
+
if (parts.length !== 3)
|
|
28
|
+
return null;
|
|
29
|
+
const iv = Buffer.from(parts[0], 'hex');
|
|
30
|
+
const encrypted = parts[1];
|
|
31
|
+
const tag = Buffer.from(parts[2], 'hex');
|
|
32
|
+
const decipher = createDecipheriv('aes-256-gcm', key, iv);
|
|
33
|
+
decipher.setAuthTag(tag);
|
|
34
|
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
35
|
+
decrypted += decipher.final('utf8');
|
|
36
|
+
return decrypted;
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
// Return null on decryption/integrity failures
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* High-performance Secure Session Middleware.
|
|
45
|
+
* Supports stateless encrypted cookies (AES-256-GCM) and stateful shared session stores.
|
|
46
|
+
*/
|
|
47
|
+
export function session(options) {
|
|
48
|
+
if (!options || !options.secret) {
|
|
49
|
+
throw new Error('[@voltrix/security] Session secret is required');
|
|
50
|
+
}
|
|
51
|
+
const key = deriveKey(options.secret);
|
|
52
|
+
const cookieName = options.cookieName ?? 'voltrix_session';
|
|
53
|
+
const ttlMs = options.ttlMs ?? 86400000; // 24h
|
|
54
|
+
const store = options.store;
|
|
55
|
+
const cookieOpts = options.cookieOptions ?? {
|
|
56
|
+
path: '/',
|
|
57
|
+
secure: false, // Default false, developers toggle true in production
|
|
58
|
+
httpOnly: true, // Prevents XSS script execution reads
|
|
59
|
+
sameSite: 'Lax'
|
|
60
|
+
};
|
|
61
|
+
return async (req, res, next) => {
|
|
62
|
+
const cookies = parseCookies(req.header('cookie'));
|
|
63
|
+
const sessionCookie = cookies[cookieName];
|
|
64
|
+
let sessionData = {};
|
|
65
|
+
let sessionId = null;
|
|
66
|
+
let originalSessionJson = '{}';
|
|
67
|
+
try {
|
|
68
|
+
if (sessionCookie) {
|
|
69
|
+
if (store) {
|
|
70
|
+
// Stateful Store Mode: sessionCookie holds the encrypted sessionId
|
|
71
|
+
const decryptedId = decrypt(sessionCookie, key);
|
|
72
|
+
if (decryptedId) {
|
|
73
|
+
sessionId = decryptedId;
|
|
74
|
+
const dataStr = await store.get(`voltrix:security:session:${sessionId}`);
|
|
75
|
+
if (dataStr) {
|
|
76
|
+
sessionData = JSON.parse(dataStr);
|
|
77
|
+
originalSessionJson = dataStr;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
// Stateless Cookie Mode: sessionCookie holds the encrypted session payload
|
|
83
|
+
const decryptedData = decrypt(sessionCookie, key);
|
|
84
|
+
if (decryptedData) {
|
|
85
|
+
sessionData = JSON.parse(decryptedData);
|
|
86
|
+
originalSessionJson = decryptedData;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
sessionData = {};
|
|
93
|
+
}
|
|
94
|
+
// Initialize session ID if not set (for store mode)
|
|
95
|
+
if (store && !sessionId) {
|
|
96
|
+
sessionId = randomBytes(24).toString('base64url');
|
|
97
|
+
}
|
|
98
|
+
// Attach session container to the request context
|
|
99
|
+
if (!req.context)
|
|
100
|
+
req.context = {};
|
|
101
|
+
req.context.session = sessionData;
|
|
102
|
+
// Hijack response flushing methods to automatically serialize and save the session if it changed
|
|
103
|
+
let isSaved = false;
|
|
104
|
+
const saveSessionSync = () => {
|
|
105
|
+
if (isSaved)
|
|
106
|
+
return;
|
|
107
|
+
isSaved = true;
|
|
108
|
+
try {
|
|
109
|
+
const currentSessionJson = JSON.stringify(req.context.session ?? {});
|
|
110
|
+
// Save only if session data was modified to eliminate redundant writes
|
|
111
|
+
if (currentSessionJson !== originalSessionJson) {
|
|
112
|
+
if (store && sessionId) {
|
|
113
|
+
// Stateful Save (Fire-and-forget in the background to ensure zero latency on network flushes)
|
|
114
|
+
store.set(`voltrix:security:session:${sessionId}`, currentSessionJson, ttlMs).catch(() => { });
|
|
115
|
+
const encryptedCookie = encrypt(sessionId, key);
|
|
116
|
+
res.setHeader('Set-Cookie', serializeCookie(cookieName, encryptedCookie, {
|
|
117
|
+
...cookieOpts,
|
|
118
|
+
maxAge: Math.floor(ttlMs / 1000)
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
// Stateless Save (purely synchronous GCM encryption)
|
|
123
|
+
const encryptedCookie = encrypt(currentSessionJson, key);
|
|
124
|
+
res.setHeader('Set-Cookie', serializeCookie(cookieName, encryptedCookie, {
|
|
125
|
+
...cookieOpts,
|
|
126
|
+
maxAge: Math.floor(ttlMs / 1000)
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
// Suppress session saving crashes
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
const originalEnd = res.end;
|
|
136
|
+
res.end = function (chunk) {
|
|
137
|
+
saveSessionSync();
|
|
138
|
+
originalEnd.call(this, chunk);
|
|
139
|
+
};
|
|
140
|
+
if (res.json) {
|
|
141
|
+
const originalJson = res.json;
|
|
142
|
+
res.json = function (data, ...args) {
|
|
143
|
+
saveSessionSync();
|
|
144
|
+
originalJson.apply(this, [data, ...args]);
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (res.send) {
|
|
148
|
+
const originalSend = res.send;
|
|
149
|
+
res.send = function (body, ...args) {
|
|
150
|
+
saveSessionSync();
|
|
151
|
+
originalSend.apply(this, [body, ...args]);
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
next();
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/modules/session.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/store/index.ts"],"names":[],"mappings":"AACA,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { SecurityStore } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Zero-allocation, high-performance in-memory cache store.
|
|
4
|
+
* Incorporates a lazy-evaluation expiration check and background sweeps.
|
|
5
|
+
*/
|
|
6
|
+
export declare class MemoryStore implements SecurityStore {
|
|
7
|
+
private readonly _map;
|
|
8
|
+
private readonly _sweepInterval;
|
|
9
|
+
constructor(sweepMs?: number);
|
|
10
|
+
get(key: string): Promise<string | null>;
|
|
11
|
+
set(key: string, value: string, ttlMs: number): Promise<void>;
|
|
12
|
+
increment(key: string, ttlMs: number): Promise<number>;
|
|
13
|
+
decrement(key: string): Promise<void>;
|
|
14
|
+
delete(key: string): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Sweeps expired cache entries out of memory to prevent heap leaks.
|
|
17
|
+
*/
|
|
18
|
+
sweep(): void;
|
|
19
|
+
/**
|
|
20
|
+
* Clean up background resources.
|
|
21
|
+
*/
|
|
22
|
+
close(): void;
|
|
23
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-allocation, high-performance in-memory cache store.
|
|
3
|
+
* Incorporates a lazy-evaluation expiration check and background sweeps.
|
|
4
|
+
*/
|
|
5
|
+
export class MemoryStore {
|
|
6
|
+
_map = new Map();
|
|
7
|
+
_sweepInterval = null;
|
|
8
|
+
constructor(sweepMs = 30000) {
|
|
9
|
+
this._sweepInterval = setInterval(() => this.sweep(), sweepMs);
|
|
10
|
+
// Unref the timer so it doesn't prevent Node.js process from exiting cleanly
|
|
11
|
+
if (this._sweepInterval && typeof this._sweepInterval.unref === 'function') {
|
|
12
|
+
this._sweepInterval.unref();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
async get(key) {
|
|
16
|
+
const entry = this._map.get(key);
|
|
17
|
+
if (!entry)
|
|
18
|
+
return null;
|
|
19
|
+
if (Date.now() > entry.expiresAt) {
|
|
20
|
+
this._map.delete(key);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
return entry.value;
|
|
24
|
+
}
|
|
25
|
+
async set(key, value, ttlMs) {
|
|
26
|
+
this._map.set(key, {
|
|
27
|
+
value,
|
|
28
|
+
expiresAt: Date.now() + ttlMs
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
async increment(key, ttlMs) {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
const entry = this._map.get(key);
|
|
34
|
+
if (!entry || now > entry.expiresAt) {
|
|
35
|
+
this._map.set(key, {
|
|
36
|
+
value: '1',
|
|
37
|
+
expiresAt: now + ttlMs
|
|
38
|
+
});
|
|
39
|
+
return 1;
|
|
40
|
+
}
|
|
41
|
+
const val = parseInt(entry.value, 10) + 1;
|
|
42
|
+
entry.value = String(val);
|
|
43
|
+
return val;
|
|
44
|
+
}
|
|
45
|
+
async decrement(key) {
|
|
46
|
+
const entry = this._map.get(key);
|
|
47
|
+
if (!entry || Date.now() > entry.expiresAt)
|
|
48
|
+
return;
|
|
49
|
+
const val = parseInt(entry.value, 10) - 1;
|
|
50
|
+
entry.value = String(val);
|
|
51
|
+
}
|
|
52
|
+
async delete(key) {
|
|
53
|
+
this._map.delete(key);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Sweeps expired cache entries out of memory to prevent heap leaks.
|
|
57
|
+
*/
|
|
58
|
+
sweep() {
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
for (const [key, entry] of this._map.entries()) {
|
|
61
|
+
if (now > entry.expiresAt) {
|
|
62
|
+
this._map.delete(key);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Clean up background resources.
|
|
68
|
+
*/
|
|
69
|
+
close() {
|
|
70
|
+
if (this._sweepInterval) {
|
|
71
|
+
clearInterval(this._sweepInterval);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=memory-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-store.js","sourceRoot":"","sources":["../../src/store/memory-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Redis, type RedisOptions } from 'ioredis';
|
|
2
|
+
import type { SecurityStore } from '../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* High-performance, distributed cache store utilizing Redis.
|
|
5
|
+
* Employs a custom atomic Lua script for sliding-window rate limiting increments.
|
|
6
|
+
*/
|
|
7
|
+
export declare class RedisStore implements SecurityStore {
|
|
8
|
+
readonly redis: Redis;
|
|
9
|
+
private readonly _ownConnection;
|
|
10
|
+
constructor(redisClient: RedisOptions | Redis);
|
|
11
|
+
get(key: string): Promise<string | null>;
|
|
12
|
+
set(key: string, value: string, ttlMs: number): Promise<void>;
|
|
13
|
+
increment(key: string, ttlMs: number): Promise<number>;
|
|
14
|
+
decrement(key: string): Promise<void>;
|
|
15
|
+
delete(key: string): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Close connections if this store initialized its own Redis client.
|
|
18
|
+
*/
|
|
19
|
+
close(): Promise<void>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Redis } from 'ioredis';
|
|
2
|
+
/**
|
|
3
|
+
* High-performance, distributed cache store utilizing Redis.
|
|
4
|
+
* Employs a custom atomic Lua script for sliding-window rate limiting increments.
|
|
5
|
+
*/
|
|
6
|
+
export class RedisStore {
|
|
7
|
+
redis;
|
|
8
|
+
_ownConnection = false;
|
|
9
|
+
constructor(redisClient) {
|
|
10
|
+
if (redisClient instanceof Redis) {
|
|
11
|
+
this.redis = redisClient;
|
|
12
|
+
this._ownConnection = false;
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
this.redis = new Redis(redisClient);
|
|
16
|
+
this._ownConnection = true;
|
|
17
|
+
}
|
|
18
|
+
// Register atomic Lua script for rate limiter increments
|
|
19
|
+
this.redis.defineCommand('voltrixSecurityIncrement', {
|
|
20
|
+
numberOfKeys: 1,
|
|
21
|
+
lua: `
|
|
22
|
+
local current = redis.call('INCR', KEYS[1])
|
|
23
|
+
if current == 1 then
|
|
24
|
+
redis.call('PEXPIRE', KEYS[1], ARGV[1])
|
|
25
|
+
end
|
|
26
|
+
return current
|
|
27
|
+
`
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
async get(key) {
|
|
31
|
+
return this.redis.get(key);
|
|
32
|
+
}
|
|
33
|
+
async set(key, value, ttlMs) {
|
|
34
|
+
await this.redis.set(key, value, 'PX', ttlMs);
|
|
35
|
+
}
|
|
36
|
+
async increment(key, ttlMs) {
|
|
37
|
+
// Invoke the custom registered atomic Lua command
|
|
38
|
+
return this.redis.voltrixSecurityIncrement(key, String(ttlMs));
|
|
39
|
+
}
|
|
40
|
+
async decrement(key) {
|
|
41
|
+
await this.redis.decr(key);
|
|
42
|
+
}
|
|
43
|
+
async delete(key) {
|
|
44
|
+
await this.redis.del(key);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Close connections if this store initialized its own Redis client.
|
|
48
|
+
*/
|
|
49
|
+
async close() {
|
|
50
|
+
if (this._ownConnection) {
|
|
51
|
+
await this.redis.quit();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=redis-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-store.js","sourceRoot":"","sources":["../../src/store/redis-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAqB,MAAM,SAAS,CAAC;AAGnD;;;GAGG;AACH,MAAM,OAAO,UAAU;IACL,KAAK,CAAQ;IACZ,cAAc,GAAY,KAAK,CAAC;IAEjD,YAAY,WAAiC;QAC3C,IAAI,WAAW,YAAY,KAAK,EAAE,CAAC;YACjC,IAAI,CAAC,KAAK,GAAG,WAAW,CAAC;YACzB,IAAI,CAAC,cAAc,GAAG,KAAK,CAAC;QAC9B,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,KAAK,GAAG,IAAI,KAAK,CAAC,WAA2B,CAAC,CAAC;YACpD,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;QAC7B,CAAC;QAED,yDAAyD;QACzD,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,0BAA0B,EAAE;YACnD,YAAY,EAAE,CAAC;YACf,GAAG,EAAE;;;;;;OAMJ;SACF,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW;QACnB,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC7B,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW,EAAE,KAAa,EAAE,KAAa;QACjD,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;IAChD,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,GAAW,EAAE,KAAa;QACxC,kDAAkD;QAClD,OAAQ,IAAI,CAAC,KAAa,CAAC,wBAAwB,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAC1E,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,GAAW;QACzB,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC7B,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC5B,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,KAAK;QACT,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACxB,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QAC1B,CAAC;IACH,CAAC;CACF"}
|