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.
Files changed (170) hide show
  1. package/docker-compose.dev.yml +6 -3
  2. package/package.json +1 -1
  3. package/server/index.js +229 -14
  4. package/server/lib/compliance/control-mapper.js +401 -0
  5. package/server/lib/compliance/control-mapper.test.js +117 -0
  6. package/server/lib/compliance/evidence-linker.js +296 -0
  7. package/server/lib/compliance/evidence-linker.test.js +121 -0
  8. package/server/lib/compliance/gdpr-checklist.js +416 -0
  9. package/server/lib/compliance/gdpr-checklist.test.js +131 -0
  10. package/server/lib/compliance/hipaa-checklist.js +277 -0
  11. package/server/lib/compliance/hipaa-checklist.test.js +101 -0
  12. package/server/lib/compliance/iso27001-checklist.js +287 -0
  13. package/server/lib/compliance/iso27001-checklist.test.js +99 -0
  14. package/server/lib/compliance/multi-framework-reporter.js +284 -0
  15. package/server/lib/compliance/multi-framework-reporter.test.js +127 -0
  16. package/server/lib/compliance/pci-dss-checklist.js +214 -0
  17. package/server/lib/compliance/pci-dss-checklist.test.js +95 -0
  18. package/server/lib/compliance/trust-centre.js +187 -0
  19. package/server/lib/compliance/trust-centre.test.js +93 -0
  20. package/server/lib/dashboard/api-server.js +155 -0
  21. package/server/lib/dashboard/api-server.test.js +155 -0
  22. package/server/lib/dashboard/health-api.js +199 -0
  23. package/server/lib/dashboard/health-api.test.js +122 -0
  24. package/server/lib/dashboard/notes-api.js +234 -0
  25. package/server/lib/dashboard/notes-api.test.js +134 -0
  26. package/server/lib/dashboard/router-api.js +176 -0
  27. package/server/lib/dashboard/router-api.test.js +132 -0
  28. package/server/lib/dashboard/tasks-api.js +289 -0
  29. package/server/lib/dashboard/tasks-api.test.js +161 -0
  30. package/server/lib/dashboard/tlc-introspection.js +197 -0
  31. package/server/lib/dashboard/tlc-introspection.test.js +138 -0
  32. package/server/lib/dashboard/version-api.js +222 -0
  33. package/server/lib/dashboard/version-api.test.js +112 -0
  34. package/server/lib/dashboard/websocket-server.js +104 -0
  35. package/server/lib/dashboard/websocket-server.test.js +118 -0
  36. package/server/lib/deploy/branch-classifier.js +163 -0
  37. package/server/lib/deploy/branch-classifier.test.js +164 -0
  38. package/server/lib/deploy/deployment-approval.js +299 -0
  39. package/server/lib/deploy/deployment-approval.test.js +296 -0
  40. package/server/lib/deploy/deployment-audit.js +374 -0
  41. package/server/lib/deploy/deployment-audit.test.js +307 -0
  42. package/server/lib/deploy/deployment-executor.js +335 -0
  43. package/server/lib/deploy/deployment-executor.test.js +329 -0
  44. package/server/lib/deploy/deployment-rules.js +163 -0
  45. package/server/lib/deploy/deployment-rules.test.js +188 -0
  46. package/server/lib/deploy/rollback-manager.js +379 -0
  47. package/server/lib/deploy/rollback-manager.test.js +321 -0
  48. package/server/lib/deploy/security-gates.js +236 -0
  49. package/server/lib/deploy/security-gates.test.js +222 -0
  50. package/server/lib/k8s/gitops-config.js +188 -0
  51. package/server/lib/k8s/gitops-config.test.js +59 -0
  52. package/server/lib/k8s/helm-generator.js +196 -0
  53. package/server/lib/k8s/helm-generator.test.js +59 -0
  54. package/server/lib/k8s/kustomize-generator.js +176 -0
  55. package/server/lib/k8s/kustomize-generator.test.js +58 -0
  56. package/server/lib/k8s/network-policy.js +114 -0
  57. package/server/lib/k8s/network-policy.test.js +53 -0
  58. package/server/lib/k8s/pod-security.js +114 -0
  59. package/server/lib/k8s/pod-security.test.js +55 -0
  60. package/server/lib/k8s/rbac-generator.js +132 -0
  61. package/server/lib/k8s/rbac-generator.test.js +57 -0
  62. package/server/lib/k8s/resource-manager.js +172 -0
  63. package/server/lib/k8s/resource-manager.test.js +60 -0
  64. package/server/lib/k8s/secrets-encryption.js +168 -0
  65. package/server/lib/k8s/secrets-encryption.test.js +49 -0
  66. package/server/lib/monitoring/alert-manager.js +238 -0
  67. package/server/lib/monitoring/alert-manager.test.js +106 -0
  68. package/server/lib/monitoring/health-check.js +226 -0
  69. package/server/lib/monitoring/health-check.test.js +176 -0
  70. package/server/lib/monitoring/incident-manager.js +230 -0
  71. package/server/lib/monitoring/incident-manager.test.js +98 -0
  72. package/server/lib/monitoring/log-aggregator.js +147 -0
  73. package/server/lib/monitoring/log-aggregator.test.js +89 -0
  74. package/server/lib/monitoring/metrics-collector.js +337 -0
  75. package/server/lib/monitoring/metrics-collector.test.js +172 -0
  76. package/server/lib/monitoring/status-page.js +214 -0
  77. package/server/lib/monitoring/status-page.test.js +105 -0
  78. package/server/lib/monitoring/uptime-monitor.js +194 -0
  79. package/server/lib/monitoring/uptime-monitor.test.js +109 -0
  80. package/server/lib/network/fail2ban-config.js +294 -0
  81. package/server/lib/network/fail2ban-config.test.js +275 -0
  82. package/server/lib/network/firewall-manager.js +252 -0
  83. package/server/lib/network/firewall-manager.test.js +254 -0
  84. package/server/lib/network/geoip-filter.js +282 -0
  85. package/server/lib/network/geoip-filter.test.js +264 -0
  86. package/server/lib/network/rate-limiter.js +229 -0
  87. package/server/lib/network/rate-limiter.test.js +293 -0
  88. package/server/lib/network/request-validator.js +351 -0
  89. package/server/lib/network/request-validator.test.js +345 -0
  90. package/server/lib/network/security-headers.js +251 -0
  91. package/server/lib/network/security-headers.test.js +283 -0
  92. package/server/lib/network/tls-config.js +210 -0
  93. package/server/lib/network/tls-config.test.js +248 -0
  94. package/server/lib/security/auth-security.js +369 -0
  95. package/server/lib/security/auth-security.test.js +448 -0
  96. package/server/lib/security/cis-benchmark.js +152 -0
  97. package/server/lib/security/cis-benchmark.test.js +137 -0
  98. package/server/lib/security/compose-templates.js +312 -0
  99. package/server/lib/security/compose-templates.test.js +229 -0
  100. package/server/lib/security/container-runtime.js +456 -0
  101. package/server/lib/security/container-runtime.test.js +503 -0
  102. package/server/lib/security/cors-validator.js +278 -0
  103. package/server/lib/security/cors-validator.test.js +310 -0
  104. package/server/lib/security/crypto-utils.js +253 -0
  105. package/server/lib/security/crypto-utils.test.js +409 -0
  106. package/server/lib/security/dockerfile-linter.js +459 -0
  107. package/server/lib/security/dockerfile-linter.test.js +483 -0
  108. package/server/lib/security/dockerfile-templates.js +278 -0
  109. package/server/lib/security/dockerfile-templates.test.js +164 -0
  110. package/server/lib/security/error-sanitizer.js +426 -0
  111. package/server/lib/security/error-sanitizer.test.js +331 -0
  112. package/server/lib/security/headers-generator.js +368 -0
  113. package/server/lib/security/headers-generator.test.js +398 -0
  114. package/server/lib/security/image-scanner.js +83 -0
  115. package/server/lib/security/image-scanner.test.js +106 -0
  116. package/server/lib/security/input-validator.js +352 -0
  117. package/server/lib/security/input-validator.test.js +330 -0
  118. package/server/lib/security/network-policy.js +174 -0
  119. package/server/lib/security/network-policy.test.js +164 -0
  120. package/server/lib/security/output-encoder.js +237 -0
  121. package/server/lib/security/output-encoder.test.js +276 -0
  122. package/server/lib/security/path-validator.js +359 -0
  123. package/server/lib/security/path-validator.test.js +293 -0
  124. package/server/lib/security/query-builder.js +421 -0
  125. package/server/lib/security/query-builder.test.js +318 -0
  126. package/server/lib/security/secret-detector.js +290 -0
  127. package/server/lib/security/secret-detector.test.js +354 -0
  128. package/server/lib/security/secrets-validator.js +137 -0
  129. package/server/lib/security/secrets-validator.test.js +120 -0
  130. package/server/lib/security-testing/dast-runner.js +154 -0
  131. package/server/lib/security-testing/dast-runner.test.js +62 -0
  132. package/server/lib/security-testing/dependency-scanner.js +172 -0
  133. package/server/lib/security-testing/dependency-scanner.test.js +64 -0
  134. package/server/lib/security-testing/pentest-runner.js +230 -0
  135. package/server/lib/security-testing/pentest-runner.test.js +60 -0
  136. package/server/lib/security-testing/sast-runner.js +136 -0
  137. package/server/lib/security-testing/sast-runner.test.js +62 -0
  138. package/server/lib/security-testing/secret-scanner.js +153 -0
  139. package/server/lib/security-testing/secret-scanner.test.js +66 -0
  140. package/server/lib/security-testing/security-gate.js +216 -0
  141. package/server/lib/security-testing/security-gate.test.js +115 -0
  142. package/server/lib/security-testing/security-reporter.js +303 -0
  143. package/server/lib/security-testing/security-reporter.test.js +114 -0
  144. package/server/lib/standards/audit-checker.js +546 -0
  145. package/server/lib/standards/audit-checker.test.js +415 -0
  146. package/server/lib/standards/cleanup-executor.js +452 -0
  147. package/server/lib/standards/cleanup-executor.test.js +293 -0
  148. package/server/lib/standards/refactor-stepper.js +425 -0
  149. package/server/lib/standards/refactor-stepper.test.js +298 -0
  150. package/server/lib/standards/standards-injector.js +167 -0
  151. package/server/lib/standards/standards-injector.test.js +232 -0
  152. package/server/lib/user-management.test.js +284 -0
  153. package/server/lib/vps/backup-manager.js +157 -0
  154. package/server/lib/vps/backup-manager.test.js +59 -0
  155. package/server/lib/vps/caddy-config.js +159 -0
  156. package/server/lib/vps/caddy-config.test.js +48 -0
  157. package/server/lib/vps/compose-orchestrator.js +219 -0
  158. package/server/lib/vps/compose-orchestrator.test.js +50 -0
  159. package/server/lib/vps/database-config.js +208 -0
  160. package/server/lib/vps/database-config.test.js +47 -0
  161. package/server/lib/vps/deploy-script.js +211 -0
  162. package/server/lib/vps/deploy-script.test.js +53 -0
  163. package/server/lib/vps/secrets-manager.js +148 -0
  164. package/server/lib/vps/secrets-manager.test.js +58 -0
  165. package/server/lib/vps/server-hardening.js +174 -0
  166. package/server/lib/vps/server-hardening.test.js +70 -0
  167. package/server/package-lock.json +19 -0
  168. package/server/package.json +1 -0
  169. package/server/templates/CLAUDE.md +37 -0
  170. 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
+ });