tlc-claude-code 1.4.7 → 1.4.9
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/docker-compose.dev.yml +6 -3
- package/package.json +1 -1
- package/server/index.js +229 -14
- package/server/lib/compliance/control-mapper.js +401 -0
- package/server/lib/compliance/control-mapper.test.js +117 -0
- package/server/lib/compliance/evidence-linker.js +296 -0
- package/server/lib/compliance/evidence-linker.test.js +121 -0
- package/server/lib/compliance/gdpr-checklist.js +416 -0
- package/server/lib/compliance/gdpr-checklist.test.js +131 -0
- package/server/lib/compliance/hipaa-checklist.js +277 -0
- package/server/lib/compliance/hipaa-checklist.test.js +101 -0
- package/server/lib/compliance/iso27001-checklist.js +287 -0
- package/server/lib/compliance/iso27001-checklist.test.js +99 -0
- package/server/lib/compliance/multi-framework-reporter.js +284 -0
- package/server/lib/compliance/multi-framework-reporter.test.js +127 -0
- package/server/lib/compliance/pci-dss-checklist.js +214 -0
- package/server/lib/compliance/pci-dss-checklist.test.js +95 -0
- package/server/lib/compliance/trust-centre.js +187 -0
- package/server/lib/compliance/trust-centre.test.js +93 -0
- package/server/lib/dashboard/api-server.js +155 -0
- package/server/lib/dashboard/api-server.test.js +155 -0
- package/server/lib/dashboard/health-api.js +199 -0
- package/server/lib/dashboard/health-api.test.js +122 -0
- package/server/lib/dashboard/notes-api.js +234 -0
- package/server/lib/dashboard/notes-api.test.js +134 -0
- package/server/lib/dashboard/router-api.js +176 -0
- package/server/lib/dashboard/router-api.test.js +132 -0
- package/server/lib/dashboard/tasks-api.js +289 -0
- package/server/lib/dashboard/tasks-api.test.js +161 -0
- package/server/lib/dashboard/tlc-introspection.js +197 -0
- package/server/lib/dashboard/tlc-introspection.test.js +138 -0
- package/server/lib/dashboard/version-api.js +222 -0
- package/server/lib/dashboard/version-api.test.js +112 -0
- package/server/lib/dashboard/websocket-server.js +104 -0
- package/server/lib/dashboard/websocket-server.test.js +118 -0
- package/server/lib/deploy/branch-classifier.js +163 -0
- package/server/lib/deploy/branch-classifier.test.js +164 -0
- package/server/lib/deploy/deployment-approval.js +299 -0
- package/server/lib/deploy/deployment-approval.test.js +296 -0
- package/server/lib/deploy/deployment-audit.js +374 -0
- package/server/lib/deploy/deployment-audit.test.js +307 -0
- package/server/lib/deploy/deployment-executor.js +335 -0
- package/server/lib/deploy/deployment-executor.test.js +329 -0
- package/server/lib/deploy/deployment-rules.js +163 -0
- package/server/lib/deploy/deployment-rules.test.js +188 -0
- package/server/lib/deploy/rollback-manager.js +379 -0
- package/server/lib/deploy/rollback-manager.test.js +321 -0
- package/server/lib/deploy/security-gates.js +236 -0
- package/server/lib/deploy/security-gates.test.js +222 -0
- package/server/lib/k8s/gitops-config.js +188 -0
- package/server/lib/k8s/gitops-config.test.js +59 -0
- package/server/lib/k8s/helm-generator.js +196 -0
- package/server/lib/k8s/helm-generator.test.js +59 -0
- package/server/lib/k8s/kustomize-generator.js +176 -0
- package/server/lib/k8s/kustomize-generator.test.js +58 -0
- package/server/lib/k8s/network-policy.js +114 -0
- package/server/lib/k8s/network-policy.test.js +53 -0
- package/server/lib/k8s/pod-security.js +114 -0
- package/server/lib/k8s/pod-security.test.js +55 -0
- package/server/lib/k8s/rbac-generator.js +132 -0
- package/server/lib/k8s/rbac-generator.test.js +57 -0
- package/server/lib/k8s/resource-manager.js +172 -0
- package/server/lib/k8s/resource-manager.test.js +60 -0
- package/server/lib/k8s/secrets-encryption.js +168 -0
- package/server/lib/k8s/secrets-encryption.test.js +49 -0
- package/server/lib/monitoring/alert-manager.js +238 -0
- package/server/lib/monitoring/alert-manager.test.js +106 -0
- package/server/lib/monitoring/health-check.js +226 -0
- package/server/lib/monitoring/health-check.test.js +176 -0
- package/server/lib/monitoring/incident-manager.js +230 -0
- package/server/lib/monitoring/incident-manager.test.js +98 -0
- package/server/lib/monitoring/log-aggregator.js +147 -0
- package/server/lib/monitoring/log-aggregator.test.js +89 -0
- package/server/lib/monitoring/metrics-collector.js +337 -0
- package/server/lib/monitoring/metrics-collector.test.js +172 -0
- package/server/lib/monitoring/status-page.js +214 -0
- package/server/lib/monitoring/status-page.test.js +105 -0
- package/server/lib/monitoring/uptime-monitor.js +194 -0
- package/server/lib/monitoring/uptime-monitor.test.js +109 -0
- package/server/lib/network/fail2ban-config.js +294 -0
- package/server/lib/network/fail2ban-config.test.js +275 -0
- package/server/lib/network/firewall-manager.js +252 -0
- package/server/lib/network/firewall-manager.test.js +254 -0
- package/server/lib/network/geoip-filter.js +282 -0
- package/server/lib/network/geoip-filter.test.js +264 -0
- package/server/lib/network/rate-limiter.js +229 -0
- package/server/lib/network/rate-limiter.test.js +293 -0
- package/server/lib/network/request-validator.js +351 -0
- package/server/lib/network/request-validator.test.js +345 -0
- package/server/lib/network/security-headers.js +251 -0
- package/server/lib/network/security-headers.test.js +283 -0
- package/server/lib/network/tls-config.js +210 -0
- package/server/lib/network/tls-config.test.js +248 -0
- package/server/lib/security/auth-security.js +369 -0
- package/server/lib/security/auth-security.test.js +448 -0
- package/server/lib/security/cis-benchmark.js +152 -0
- package/server/lib/security/cis-benchmark.test.js +137 -0
- package/server/lib/security/compose-templates.js +312 -0
- package/server/lib/security/compose-templates.test.js +229 -0
- package/server/lib/security/container-runtime.js +456 -0
- package/server/lib/security/container-runtime.test.js +503 -0
- package/server/lib/security/cors-validator.js +278 -0
- package/server/lib/security/cors-validator.test.js +310 -0
- package/server/lib/security/crypto-utils.js +253 -0
- package/server/lib/security/crypto-utils.test.js +409 -0
- package/server/lib/security/dockerfile-linter.js +459 -0
- package/server/lib/security/dockerfile-linter.test.js +483 -0
- package/server/lib/security/dockerfile-templates.js +278 -0
- package/server/lib/security/dockerfile-templates.test.js +164 -0
- package/server/lib/security/error-sanitizer.js +426 -0
- package/server/lib/security/error-sanitizer.test.js +331 -0
- package/server/lib/security/headers-generator.js +368 -0
- package/server/lib/security/headers-generator.test.js +398 -0
- package/server/lib/security/image-scanner.js +83 -0
- package/server/lib/security/image-scanner.test.js +106 -0
- package/server/lib/security/input-validator.js +352 -0
- package/server/lib/security/input-validator.test.js +330 -0
- package/server/lib/security/network-policy.js +174 -0
- package/server/lib/security/network-policy.test.js +164 -0
- package/server/lib/security/output-encoder.js +237 -0
- package/server/lib/security/output-encoder.test.js +276 -0
- package/server/lib/security/path-validator.js +359 -0
- package/server/lib/security/path-validator.test.js +293 -0
- package/server/lib/security/query-builder.js +421 -0
- package/server/lib/security/query-builder.test.js +318 -0
- package/server/lib/security/secret-detector.js +290 -0
- package/server/lib/security/secret-detector.test.js +354 -0
- package/server/lib/security/secrets-validator.js +137 -0
- package/server/lib/security/secrets-validator.test.js +120 -0
- package/server/lib/security-testing/dast-runner.js +154 -0
- package/server/lib/security-testing/dast-runner.test.js +62 -0
- package/server/lib/security-testing/dependency-scanner.js +172 -0
- package/server/lib/security-testing/dependency-scanner.test.js +64 -0
- package/server/lib/security-testing/pentest-runner.js +230 -0
- package/server/lib/security-testing/pentest-runner.test.js +60 -0
- package/server/lib/security-testing/sast-runner.js +136 -0
- package/server/lib/security-testing/sast-runner.test.js +62 -0
- package/server/lib/security-testing/secret-scanner.js +153 -0
- package/server/lib/security-testing/secret-scanner.test.js +66 -0
- package/server/lib/security-testing/security-gate.js +216 -0
- package/server/lib/security-testing/security-gate.test.js +115 -0
- package/server/lib/security-testing/security-reporter.js +303 -0
- package/server/lib/security-testing/security-reporter.test.js +114 -0
- package/server/lib/standards/audit-checker.js +546 -0
- package/server/lib/standards/audit-checker.test.js +415 -0
- package/server/lib/standards/cleanup-executor.js +452 -0
- package/server/lib/standards/cleanup-executor.test.js +293 -0
- package/server/lib/standards/refactor-stepper.js +425 -0
- package/server/lib/standards/refactor-stepper.test.js +298 -0
- package/server/lib/standards/standards-injector.js +167 -0
- package/server/lib/standards/standards-injector.test.js +232 -0
- package/server/lib/user-management.test.js +284 -0
- package/server/lib/vps/backup-manager.js +157 -0
- package/server/lib/vps/backup-manager.test.js +59 -0
- package/server/lib/vps/caddy-config.js +159 -0
- package/server/lib/vps/caddy-config.test.js +48 -0
- package/server/lib/vps/compose-orchestrator.js +219 -0
- package/server/lib/vps/compose-orchestrator.test.js +50 -0
- package/server/lib/vps/database-config.js +208 -0
- package/server/lib/vps/database-config.test.js +47 -0
- package/server/lib/vps/deploy-script.js +211 -0
- package/server/lib/vps/deploy-script.test.js +53 -0
- package/server/lib/vps/secrets-manager.js +148 -0
- package/server/lib/vps/secrets-manager.test.js +58 -0
- package/server/lib/vps/server-hardening.js +174 -0
- package/server/lib/vps/server-hardening.test.js +70 -0
- package/server/package-lock.json +19 -0
- package/server/package.json +1 -0
- package/server/templates/CLAUDE.md +37 -0
- package/server/templates/CODING-STANDARDS.md +408 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate Limiter
|
|
3
|
+
* Sliding window rate limiting with whitelist/blacklist support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const RATE_LIMIT_ALGORITHMS = {
|
|
7
|
+
SLIDING_WINDOW: 'sliding_window',
|
|
8
|
+
TOKEN_BUCKET: 'token_bucket',
|
|
9
|
+
FIXED_WINDOW: 'fixed_window',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a sliding window rate limiter store
|
|
14
|
+
*/
|
|
15
|
+
export function createSlidingWindow(options) {
|
|
16
|
+
const { windowMs, maxRequests } = options;
|
|
17
|
+
const store = new Map();
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
increment(key) {
|
|
21
|
+
const now = Date.now();
|
|
22
|
+
const record = store.get(key);
|
|
23
|
+
|
|
24
|
+
if (!record || now - record.windowStart >= windowMs) {
|
|
25
|
+
// Start new window
|
|
26
|
+
store.set(key, { count: 1, windowStart: now });
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (record.count >= maxRequests) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
record.count++;
|
|
35
|
+
return true;
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
getCount(key) {
|
|
39
|
+
const record = store.get(key);
|
|
40
|
+
if (!record) return 0;
|
|
41
|
+
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
if (now - record.windowStart >= windowMs) {
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return record.count;
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
reset(key) {
|
|
51
|
+
if (key) {
|
|
52
|
+
store.delete(key);
|
|
53
|
+
} else {
|
|
54
|
+
store.clear();
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check if an IP matches a CIDR range
|
|
62
|
+
*/
|
|
63
|
+
function ipMatchesCidr(ip, cidr) {
|
|
64
|
+
if (!cidr.includes('/')) {
|
|
65
|
+
return ip === cidr;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const [range, bits] = cidr.split('/');
|
|
69
|
+
const mask = parseInt(bits, 10);
|
|
70
|
+
|
|
71
|
+
const ipParts = ip.split('.').map(Number);
|
|
72
|
+
const rangeParts = range.split('.').map(Number);
|
|
73
|
+
|
|
74
|
+
const ipNum = (ipParts[0] << 24) | (ipParts[1] << 16) | (ipParts[2] << 8) | ipParts[3];
|
|
75
|
+
const rangeNum =
|
|
76
|
+
(rangeParts[0] << 24) | (rangeParts[1] << 16) | (rangeParts[2] << 8) | rangeParts[3];
|
|
77
|
+
|
|
78
|
+
const maskNum = ~((1 << (32 - mask)) - 1);
|
|
79
|
+
|
|
80
|
+
return (ipNum & maskNum) === (rangeNum & maskNum);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if IP is in whitelist
|
|
85
|
+
*/
|
|
86
|
+
export function isWhitelisted(ip, whitelist) {
|
|
87
|
+
if (!whitelist || whitelist.length === 0) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return whitelist.some((entry) => ipMatchesCidr(ip, entry));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check if IP is in blacklist
|
|
96
|
+
*/
|
|
97
|
+
export function isBlacklisted(ip, blacklist) {
|
|
98
|
+
if (!blacklist || blacklist.length === 0) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return blacklist.some((entry) => ipMatchesCidr(ip, entry));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check rate limit for a request
|
|
107
|
+
*/
|
|
108
|
+
export function checkRateLimit(options) {
|
|
109
|
+
const { ip, endpoint, limits, store } = options;
|
|
110
|
+
|
|
111
|
+
// Find the limit configuration for this endpoint
|
|
112
|
+
const limitConfig = limits[endpoint] || limits.default || { maxRequests: 100, windowMs: 60000 };
|
|
113
|
+
const { maxRequests, windowMs } = limitConfig;
|
|
114
|
+
|
|
115
|
+
const key = `${ip}:${endpoint}`;
|
|
116
|
+
const now = Date.now();
|
|
117
|
+
|
|
118
|
+
// Get or create record
|
|
119
|
+
let record = store.get(key);
|
|
120
|
+
if (!record || now - record.windowStart >= windowMs) {
|
|
121
|
+
record = { count: 0, windowStart: now };
|
|
122
|
+
store.set(key, record);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const resetTime = record.windowStart + windowMs;
|
|
126
|
+
|
|
127
|
+
if (record.count >= maxRequests) {
|
|
128
|
+
return {
|
|
129
|
+
allowed: false,
|
|
130
|
+
limit: maxRequests,
|
|
131
|
+
remaining: 0,
|
|
132
|
+
resetTime,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Calculate remaining before incrementing
|
|
137
|
+
const remaining = Math.max(0, maxRequests - record.count);
|
|
138
|
+
|
|
139
|
+
// Increment counter
|
|
140
|
+
record.count++;
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
allowed: true,
|
|
144
|
+
limit: maxRequests,
|
|
145
|
+
remaining,
|
|
146
|
+
resetTime,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Generate rate limit headers
|
|
152
|
+
*/
|
|
153
|
+
export function getRateLimitHeaders(options) {
|
|
154
|
+
const { limit, remaining, resetTime, blocked } = options;
|
|
155
|
+
|
|
156
|
+
const headers = {
|
|
157
|
+
'X-RateLimit-Limit': String(limit),
|
|
158
|
+
'X-RateLimit-Remaining': String(remaining),
|
|
159
|
+
'X-RateLimit-Reset': String(Math.ceil(resetTime / 1000)),
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
if (blocked) {
|
|
163
|
+
const retryAfter = Math.ceil((resetTime - Date.now()) / 1000);
|
|
164
|
+
headers['Retry-After'] = String(Math.max(1, retryAfter));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return headers;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Create a rate limiter instance
|
|
172
|
+
*/
|
|
173
|
+
export function createRateLimiter(config) {
|
|
174
|
+
const { limits, whitelist = [], blacklist = [] } = config;
|
|
175
|
+
const store = new Map();
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
check(options) {
|
|
179
|
+
const { ip, endpoint } = options;
|
|
180
|
+
|
|
181
|
+
// Check blacklist first
|
|
182
|
+
if (isBlacklisted(ip, blacklist)) {
|
|
183
|
+
return {
|
|
184
|
+
allowed: false,
|
|
185
|
+
reason: 'IP is on blacklist',
|
|
186
|
+
limit: 0,
|
|
187
|
+
remaining: 0,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Check whitelist
|
|
192
|
+
if (isWhitelisted(ip, whitelist)) {
|
|
193
|
+
// Find limit config just for the limit value
|
|
194
|
+
const limitConfig = limits[endpoint] || limits.default || { maxRequests: 100 };
|
|
195
|
+
return {
|
|
196
|
+
allowed: true,
|
|
197
|
+
limit: limitConfig.maxRequests,
|
|
198
|
+
remaining: limitConfig.maxRequests,
|
|
199
|
+
whitelisted: true,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Normal rate limiting
|
|
204
|
+
return checkRateLimit({ ip, endpoint, limits, store });
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
getHeaders(result) {
|
|
208
|
+
return getRateLimitHeaders({
|
|
209
|
+
limit: result.limit,
|
|
210
|
+
remaining: result.remaining,
|
|
211
|
+
resetTime: result.resetTime || Date.now() + 60000,
|
|
212
|
+
blocked: !result.allowed,
|
|
213
|
+
});
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
reset(ip) {
|
|
217
|
+
if (ip) {
|
|
218
|
+
// Reset all entries for this IP
|
|
219
|
+
for (const key of store.keys()) {
|
|
220
|
+
if (key.startsWith(`${ip}:`)) {
|
|
221
|
+
store.delete(key);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
store.clear();
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate Limiter Tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
createRateLimiter,
|
|
7
|
+
checkRateLimit,
|
|
8
|
+
getRateLimitHeaders,
|
|
9
|
+
RATE_LIMIT_ALGORITHMS,
|
|
10
|
+
createSlidingWindow,
|
|
11
|
+
isWhitelisted,
|
|
12
|
+
isBlacklisted,
|
|
13
|
+
} from './rate-limiter.js';
|
|
14
|
+
|
|
15
|
+
describe('rate-limiter', () => {
|
|
16
|
+
describe('RATE_LIMIT_ALGORITHMS', () => {
|
|
17
|
+
it('defines algorithm constants', () => {
|
|
18
|
+
expect(RATE_LIMIT_ALGORITHMS.SLIDING_WINDOW).toBe('sliding_window');
|
|
19
|
+
expect(RATE_LIMIT_ALGORITHMS.TOKEN_BUCKET).toBe('token_bucket');
|
|
20
|
+
expect(RATE_LIMIT_ALGORITHMS.FIXED_WINDOW).toBe('fixed_window');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('createSlidingWindow', () => {
|
|
25
|
+
it('creates sliding window counter', () => {
|
|
26
|
+
const window = createSlidingWindow({
|
|
27
|
+
windowMs: 60000,
|
|
28
|
+
maxRequests: 100,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(window.increment).toBeDefined();
|
|
32
|
+
expect(window.getCount).toBeDefined();
|
|
33
|
+
expect(window.reset).toBeDefined();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('tracks requests within window', () => {
|
|
37
|
+
const window = createSlidingWindow({
|
|
38
|
+
windowMs: 60000,
|
|
39
|
+
maxRequests: 100,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
window.increment('192.168.1.1');
|
|
43
|
+
window.increment('192.168.1.1');
|
|
44
|
+
|
|
45
|
+
expect(window.getCount('192.168.1.1')).toBe(2);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('respects max requests', () => {
|
|
49
|
+
const window = createSlidingWindow({
|
|
50
|
+
windowMs: 60000,
|
|
51
|
+
maxRequests: 2,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(window.increment('192.168.1.1')).toBe(true);
|
|
55
|
+
expect(window.increment('192.168.1.1')).toBe(true);
|
|
56
|
+
expect(window.increment('192.168.1.1')).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('resets after window expires', async () => {
|
|
60
|
+
const window = createSlidingWindow({
|
|
61
|
+
windowMs: 50,
|
|
62
|
+
maxRequests: 1,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
window.increment('192.168.1.1');
|
|
66
|
+
expect(window.increment('192.168.1.1')).toBe(false);
|
|
67
|
+
|
|
68
|
+
await new Promise((r) => setTimeout(r, 60));
|
|
69
|
+
|
|
70
|
+
expect(window.increment('192.168.1.1')).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('checkRateLimit', () => {
|
|
75
|
+
it('allows requests under limit', () => {
|
|
76
|
+
const result = checkRateLimit({
|
|
77
|
+
ip: '192.168.1.1',
|
|
78
|
+
endpoint: '/api/test',
|
|
79
|
+
limits: { '/api/test': { maxRequests: 100, windowMs: 60000 } },
|
|
80
|
+
store: new Map(),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(result.allowed).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('blocks requests over limit', () => {
|
|
87
|
+
const store = new Map();
|
|
88
|
+
store.set('192.168.1.1:/api/test', { count: 100, windowStart: Date.now() });
|
|
89
|
+
|
|
90
|
+
const result = checkRateLimit({
|
|
91
|
+
ip: '192.168.1.1',
|
|
92
|
+
endpoint: '/api/test',
|
|
93
|
+
limits: { '/api/test': { maxRequests: 100, windowMs: 60000 } },
|
|
94
|
+
store,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
expect(result.allowed).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('tracks per-endpoint limits', () => {
|
|
101
|
+
const store = new Map();
|
|
102
|
+
|
|
103
|
+
const result1 = checkRateLimit({
|
|
104
|
+
ip: '192.168.1.1',
|
|
105
|
+
endpoint: '/api/fast',
|
|
106
|
+
limits: {
|
|
107
|
+
'/api/fast': { maxRequests: 10, windowMs: 60000 },
|
|
108
|
+
'/api/slow': { maxRequests: 100, windowMs: 60000 },
|
|
109
|
+
},
|
|
110
|
+
store,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const result2 = checkRateLimit({
|
|
114
|
+
ip: '192.168.1.1',
|
|
115
|
+
endpoint: '/api/slow',
|
|
116
|
+
limits: {
|
|
117
|
+
'/api/fast': { maxRequests: 10, windowMs: 60000 },
|
|
118
|
+
'/api/slow': { maxRequests: 100, windowMs: 60000 },
|
|
119
|
+
},
|
|
120
|
+
store,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(result1.limit).toBe(10);
|
|
124
|
+
expect(result2.limit).toBe(100);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('returns remaining requests', () => {
|
|
128
|
+
const store = new Map();
|
|
129
|
+
store.set('192.168.1.1:/api/test', { count: 50, windowStart: Date.now() });
|
|
130
|
+
|
|
131
|
+
const result = checkRateLimit({
|
|
132
|
+
ip: '192.168.1.1',
|
|
133
|
+
endpoint: '/api/test',
|
|
134
|
+
limits: { '/api/test': { maxRequests: 100, windowMs: 60000 } },
|
|
135
|
+
store,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(result.remaining).toBe(50);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('getRateLimitHeaders', () => {
|
|
143
|
+
it('generates X-RateLimit-Limit header', () => {
|
|
144
|
+
const headers = getRateLimitHeaders({
|
|
145
|
+
limit: 100,
|
|
146
|
+
remaining: 50,
|
|
147
|
+
resetTime: Date.now() + 60000,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
expect(headers['X-RateLimit-Limit']).toBe('100');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('generates X-RateLimit-Remaining header', () => {
|
|
154
|
+
const headers = getRateLimitHeaders({
|
|
155
|
+
limit: 100,
|
|
156
|
+
remaining: 50,
|
|
157
|
+
resetTime: Date.now() + 60000,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
expect(headers['X-RateLimit-Remaining']).toBe('50');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('generates X-RateLimit-Reset header', () => {
|
|
164
|
+
const resetTime = Date.now() + 60000;
|
|
165
|
+
const headers = getRateLimitHeaders({
|
|
166
|
+
limit: 100,
|
|
167
|
+
remaining: 50,
|
|
168
|
+
resetTime,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(headers['X-RateLimit-Reset']).toBe(String(Math.ceil(resetTime / 1000)));
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('includes Retry-After when blocked', () => {
|
|
175
|
+
const headers = getRateLimitHeaders({
|
|
176
|
+
limit: 100,
|
|
177
|
+
remaining: 0,
|
|
178
|
+
resetTime: Date.now() + 60000,
|
|
179
|
+
blocked: true,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
expect(headers['Retry-After']).toBeDefined();
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe('isWhitelisted', () => {
|
|
187
|
+
it('returns true for whitelisted IPs', () => {
|
|
188
|
+
const result = isWhitelisted('192.168.1.1', ['192.168.1.1', '10.0.0.1']);
|
|
189
|
+
|
|
190
|
+
expect(result).toBe(true);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('returns false for non-whitelisted IPs', () => {
|
|
194
|
+
const result = isWhitelisted('192.168.1.2', ['192.168.1.1', '10.0.0.1']);
|
|
195
|
+
|
|
196
|
+
expect(result).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('supports CIDR notation', () => {
|
|
200
|
+
const result = isWhitelisted('192.168.1.50', ['192.168.1.0/24']);
|
|
201
|
+
|
|
202
|
+
expect(result).toBe(true);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('handles empty whitelist', () => {
|
|
206
|
+
const result = isWhitelisted('192.168.1.1', []);
|
|
207
|
+
|
|
208
|
+
expect(result).toBe(false);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('isBlacklisted', () => {
|
|
213
|
+
it('returns true for blacklisted IPs', () => {
|
|
214
|
+
const result = isBlacklisted('192.168.1.1', ['192.168.1.1']);
|
|
215
|
+
|
|
216
|
+
expect(result).toBe(true);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('returns false for non-blacklisted IPs', () => {
|
|
220
|
+
const result = isBlacklisted('192.168.1.2', ['192.168.1.1']);
|
|
221
|
+
|
|
222
|
+
expect(result).toBe(false);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('supports CIDR notation', () => {
|
|
226
|
+
const result = isBlacklisted('10.0.0.50', ['10.0.0.0/24']);
|
|
227
|
+
|
|
228
|
+
expect(result).toBe(true);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('createRateLimiter', () => {
|
|
233
|
+
it('creates rate limiter with methods', () => {
|
|
234
|
+
const limiter = createRateLimiter({
|
|
235
|
+
limits: {
|
|
236
|
+
default: { maxRequests: 100, windowMs: 60000 },
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
expect(limiter.check).toBeDefined();
|
|
241
|
+
expect(limiter.getHeaders).toBeDefined();
|
|
242
|
+
expect(limiter.reset).toBeDefined();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('respects whitelist', () => {
|
|
246
|
+
const limiter = createRateLimiter({
|
|
247
|
+
limits: { default: { maxRequests: 1, windowMs: 60000 } },
|
|
248
|
+
whitelist: ['192.168.1.1'],
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Exhaust limit
|
|
252
|
+
limiter.check({ ip: '192.168.1.1', endpoint: '/api/test' });
|
|
253
|
+
limiter.check({ ip: '192.168.1.1', endpoint: '/api/test' });
|
|
254
|
+
|
|
255
|
+
const result = limiter.check({ ip: '192.168.1.1', endpoint: '/api/test' });
|
|
256
|
+
expect(result.allowed).toBe(true);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('blocks blacklisted IPs immediately', () => {
|
|
260
|
+
const limiter = createRateLimiter({
|
|
261
|
+
limits: { default: { maxRequests: 100, windowMs: 60000 } },
|
|
262
|
+
blacklist: ['192.168.1.1'],
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const result = limiter.check({ ip: '192.168.1.1', endpoint: '/api/test' });
|
|
266
|
+
expect(result.allowed).toBe(false);
|
|
267
|
+
expect(result.reason).toContain('blacklist');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('uses per-endpoint limits when available', () => {
|
|
271
|
+
const limiter = createRateLimiter({
|
|
272
|
+
limits: {
|
|
273
|
+
default: { maxRequests: 100, windowMs: 60000 },
|
|
274
|
+
'/api/auth/login': { maxRequests: 5, windowMs: 60000 },
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const result = limiter.check({ ip: '192.168.1.1', endpoint: '/api/auth/login' });
|
|
279
|
+
expect(result.limit).toBe(5);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('falls back to default limit', () => {
|
|
283
|
+
const limiter = createRateLimiter({
|
|
284
|
+
limits: {
|
|
285
|
+
default: { maxRequests: 100, windowMs: 60000 },
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const result = limiter.check({ ip: '192.168.1.1', endpoint: '/api/unknown' });
|
|
290
|
+
expect(result.limit).toBe(100);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
});
|