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,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Security Module
|
|
3
|
+
*
|
|
4
|
+
* Secure authentication primitives.
|
|
5
|
+
* Addresses OWASP A07: Identification and Authentication Failures
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import crypto from 'crypto';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Argon2id parameters (OWASP recommended)
|
|
12
|
+
*/
|
|
13
|
+
const ARGON2_CONFIG = {
|
|
14
|
+
type: 2, // argon2id
|
|
15
|
+
memoryCost: 65536, // 64 MB
|
|
16
|
+
timeCost: 3,
|
|
17
|
+
parallelism: 4,
|
|
18
|
+
hashLength: 32,
|
|
19
|
+
saltLength: 16,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Hash a password using Argon2id
|
|
24
|
+
* Falls back to scrypt for compatibility if argon2 is not available
|
|
25
|
+
* @param {string} password - Password to hash
|
|
26
|
+
* @returns {Promise<string>} Hashed password
|
|
27
|
+
*/
|
|
28
|
+
export async function hashPassword(password) {
|
|
29
|
+
if (!password || password.length === 0) {
|
|
30
|
+
throw new Error('Password cannot be empty');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Generate random salt
|
|
34
|
+
const salt = crypto.randomBytes(ARGON2_CONFIG.saltLength);
|
|
35
|
+
|
|
36
|
+
// Use scrypt as a fallback (similar security, widely available)
|
|
37
|
+
// In production, you'd use argon2 library
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const keyLength = ARGON2_CONFIG.hashLength;
|
|
40
|
+
const options = {
|
|
41
|
+
N: 16384, // CPU/memory cost parameter
|
|
42
|
+
r: 8, // Block size
|
|
43
|
+
p: 1, // Parallelization
|
|
44
|
+
maxmem: ARGON2_CONFIG.memoryCost * 1024,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
crypto.scrypt(password, salt, keyLength, options, (err, derivedKey) => {
|
|
48
|
+
if (err) {
|
|
49
|
+
reject(err);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Format as Argon2id-like hash for future migration
|
|
54
|
+
const params = `m=${ARGON2_CONFIG.memoryCost},t=${ARGON2_CONFIG.timeCost},p=${ARGON2_CONFIG.parallelism}`;
|
|
55
|
+
const saltB64 = salt.toString('base64').replace(/=/g, '');
|
|
56
|
+
const hashB64 = derivedKey.toString('base64').replace(/=/g, '');
|
|
57
|
+
|
|
58
|
+
resolve(`$argon2id$v=19$${params}$${saltB64}$${hashB64}`);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Verify a password against a hash
|
|
65
|
+
* @param {string} password - Password to verify
|
|
66
|
+
* @param {string} hash - Hash to verify against
|
|
67
|
+
* @returns {Promise<boolean>} True if password matches
|
|
68
|
+
*/
|
|
69
|
+
export async function verifyPassword(password, hash) {
|
|
70
|
+
if (!password || password.length === 0) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!hash || !hash.startsWith('$argon2id$')) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
// Parse the hash
|
|
80
|
+
const parts = hash.split('$');
|
|
81
|
+
if (parts.length < 6) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Extract salt and hash
|
|
86
|
+
const saltB64 = parts[4];
|
|
87
|
+
const hashB64 = parts[5];
|
|
88
|
+
|
|
89
|
+
// Decode salt (add padding if needed)
|
|
90
|
+
const saltPadded = saltB64 + '='.repeat((4 - (saltB64.length % 4)) % 4);
|
|
91
|
+
const salt = Buffer.from(saltPadded, 'base64');
|
|
92
|
+
|
|
93
|
+
// Decode expected hash
|
|
94
|
+
const hashPadded = hashB64 + '='.repeat((4 - (hashB64.length % 4)) % 4);
|
|
95
|
+
const expectedHash = Buffer.from(hashPadded, 'base64');
|
|
96
|
+
|
|
97
|
+
// Compute hash of input password
|
|
98
|
+
return new Promise((resolve) => {
|
|
99
|
+
const options = {
|
|
100
|
+
N: 16384,
|
|
101
|
+
r: 8,
|
|
102
|
+
p: 1,
|
|
103
|
+
maxmem: ARGON2_CONFIG.memoryCost * 1024,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
crypto.scrypt(password, salt, expectedHash.length, options, (err, derivedKey) => {
|
|
107
|
+
if (err) {
|
|
108
|
+
resolve(false);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Timing-safe comparison
|
|
113
|
+
resolve(crypto.timingSafeEqual(derivedKey, expectedHash));
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
} catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Generate a cryptographically secure session token
|
|
123
|
+
* @param {Object} options - Token options
|
|
124
|
+
* @returns {string} Session token
|
|
125
|
+
*/
|
|
126
|
+
export function generateSessionToken(options = {}) {
|
|
127
|
+
const { bytes = 32, encoding = 'hex' } = options;
|
|
128
|
+
|
|
129
|
+
const buffer = crypto.randomBytes(bytes);
|
|
130
|
+
|
|
131
|
+
if (encoding === 'base64url') {
|
|
132
|
+
return buffer.toString('base64url');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return buffer.toString('hex');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Validate session token format
|
|
140
|
+
* @param {string} token - Token to validate
|
|
141
|
+
* @param {Object} options - Validation options
|
|
142
|
+
* @returns {Object} Validation result
|
|
143
|
+
*/
|
|
144
|
+
export function validateSessionToken(token, options = {}) {
|
|
145
|
+
const { expectedLength = 64 } = options;
|
|
146
|
+
|
|
147
|
+
if (!token || token.length === 0) {
|
|
148
|
+
return {
|
|
149
|
+
valid: false,
|
|
150
|
+
reason: 'Token is empty',
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (token.length !== expectedLength) {
|
|
155
|
+
return {
|
|
156
|
+
valid: false,
|
|
157
|
+
reason: `Token length must be ${expectedLength} characters`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!/^[a-f0-9]+$/i.test(token)) {
|
|
162
|
+
return {
|
|
163
|
+
valid: false,
|
|
164
|
+
reason: 'Token contains invalid characters',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { valid: true };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Create a rate limiter
|
|
173
|
+
* @param {Object} options - Rate limiter options
|
|
174
|
+
* @returns {Object} Rate limiter instance
|
|
175
|
+
*/
|
|
176
|
+
export function createRateLimiter(options = {}) {
|
|
177
|
+
const { maxAttempts = 5, windowMs = 60000 } = options;
|
|
178
|
+
|
|
179
|
+
const attempts = new Map();
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
/**
|
|
183
|
+
* Check if request is allowed
|
|
184
|
+
* @param {string} key - Identifier (e.g., email, IP)
|
|
185
|
+
* @returns {Object} Rate limit result
|
|
186
|
+
*/
|
|
187
|
+
check(key) {
|
|
188
|
+
const now = Date.now();
|
|
189
|
+
const record = attempts.get(key);
|
|
190
|
+
|
|
191
|
+
if (!record || now - record.windowStart >= windowMs) {
|
|
192
|
+
// New window
|
|
193
|
+
attempts.set(key, {
|
|
194
|
+
count: 1,
|
|
195
|
+
windowStart: now,
|
|
196
|
+
});
|
|
197
|
+
return {
|
|
198
|
+
allowed: true,
|
|
199
|
+
remaining: maxAttempts - 1,
|
|
200
|
+
retryAfter: 0,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (record.count >= maxAttempts) {
|
|
205
|
+
const retryAfter = Math.ceil((record.windowStart + windowMs - now) / 1000);
|
|
206
|
+
return {
|
|
207
|
+
allowed: false,
|
|
208
|
+
remaining: 0,
|
|
209
|
+
retryAfter,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
record.count++;
|
|
214
|
+
return {
|
|
215
|
+
allowed: true,
|
|
216
|
+
remaining: maxAttempts - record.count,
|
|
217
|
+
retryAfter: 0,
|
|
218
|
+
};
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Reset rate limit for a key
|
|
223
|
+
* @param {string} key - Identifier to reset
|
|
224
|
+
*/
|
|
225
|
+
reset(key) {
|
|
226
|
+
attempts.delete(key);
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Create an account lockout manager
|
|
233
|
+
* @param {Object} options - Lockout options
|
|
234
|
+
* @returns {Object} Account lockout instance
|
|
235
|
+
*/
|
|
236
|
+
export function createAccountLockout(options = {}) {
|
|
237
|
+
const {
|
|
238
|
+
maxFailures = 5,
|
|
239
|
+
lockoutDurationMs = 900000, // 15 minutes
|
|
240
|
+
exponentialBackoff = false,
|
|
241
|
+
} = options;
|
|
242
|
+
|
|
243
|
+
const accounts = new Map();
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
/**
|
|
247
|
+
* Record a failed login attempt
|
|
248
|
+
* @param {string} accountId - Account identifier
|
|
249
|
+
*/
|
|
250
|
+
recordFailure(accountId) {
|
|
251
|
+
const now = Date.now();
|
|
252
|
+
let record = accounts.get(accountId);
|
|
253
|
+
|
|
254
|
+
if (!record) {
|
|
255
|
+
record = {
|
|
256
|
+
failures: 0,
|
|
257
|
+
lockoutCount: 0,
|
|
258
|
+
lockedUntil: null,
|
|
259
|
+
};
|
|
260
|
+
accounts.set(accountId, record);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
record.failures++;
|
|
264
|
+
|
|
265
|
+
if (record.failures >= maxFailures) {
|
|
266
|
+
// Lock the account
|
|
267
|
+
const multiplier = exponentialBackoff ? Math.pow(2, record.lockoutCount) : 1;
|
|
268
|
+
record.lockedUntil = now + lockoutDurationMs * multiplier;
|
|
269
|
+
record.lockoutCount++;
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Record a successful login
|
|
275
|
+
* @param {string} accountId - Account identifier
|
|
276
|
+
*/
|
|
277
|
+
recordSuccess(accountId) {
|
|
278
|
+
const record = accounts.get(accountId);
|
|
279
|
+
if (record) {
|
|
280
|
+
record.failures = 0;
|
|
281
|
+
record.lockedUntil = null;
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Check if account is locked
|
|
287
|
+
* @param {string} accountId - Account identifier
|
|
288
|
+
* @returns {Object} Lock status
|
|
289
|
+
*/
|
|
290
|
+
isLocked(accountId) {
|
|
291
|
+
const now = Date.now();
|
|
292
|
+
const record = accounts.get(accountId);
|
|
293
|
+
|
|
294
|
+
if (!record || !record.lockedUntil) {
|
|
295
|
+
return { locked: false };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (now >= record.lockedUntil) {
|
|
299
|
+
// Lockout expired, reset failures but keep lockout count for backoff
|
|
300
|
+
record.failures = 0;
|
|
301
|
+
record.lockedUntil = null;
|
|
302
|
+
return { locked: false };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
locked: true,
|
|
307
|
+
unlockAt: record.lockedUntil,
|
|
308
|
+
};
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Generate secure cookie options
|
|
315
|
+
* @param {Object} options - Cookie options
|
|
316
|
+
* @returns {Object} Cookie configuration
|
|
317
|
+
*/
|
|
318
|
+
export function generateCookieOptions(options = {}) {
|
|
319
|
+
const {
|
|
320
|
+
production = false,
|
|
321
|
+
sameSite = 'Strict',
|
|
322
|
+
maxAge,
|
|
323
|
+
domain,
|
|
324
|
+
path = '/',
|
|
325
|
+
} = options;
|
|
326
|
+
|
|
327
|
+
const cookieOptions = {
|
|
328
|
+
httpOnly: true,
|
|
329
|
+
secure: production,
|
|
330
|
+
sameSite,
|
|
331
|
+
path,
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
if (maxAge !== undefined) {
|
|
335
|
+
cookieOptions.maxAge = maxAge;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (domain) {
|
|
339
|
+
cookieOptions.domain = domain;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return cookieOptions;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Timing-safe string comparison
|
|
347
|
+
* @param {string} a - First string
|
|
348
|
+
* @param {string} b - Second string
|
|
349
|
+
* @returns {boolean} True if strings are equal
|
|
350
|
+
*/
|
|
351
|
+
export function timingSafeCompare(a, b) {
|
|
352
|
+
if (typeof a !== 'string' || typeof b !== 'string') {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Use constant-time comparison
|
|
357
|
+
const bufA = Buffer.from(a);
|
|
358
|
+
const bufB = Buffer.from(b);
|
|
359
|
+
|
|
360
|
+
// If lengths differ, compare against dummy buffer of same length
|
|
361
|
+
// to avoid timing leak from length difference
|
|
362
|
+
if (bufA.length !== bufB.length) {
|
|
363
|
+
// Do a dummy comparison to maintain constant time
|
|
364
|
+
crypto.timingSafeEqual(bufA, bufA);
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return crypto.timingSafeEqual(bufA, bufB);
|
|
369
|
+
}
|