@parsrun/auth 0.1.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/README.md +133 -0
- package/dist/adapters/hono.d.ts +9 -0
- package/dist/adapters/hono.js +6 -0
- package/dist/adapters/hono.js.map +1 -0
- package/dist/adapters/index.d.ts +9 -0
- package/dist/adapters/index.js +7 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/authorization-By1Xp8Za.d.ts +213 -0
- package/dist/base-BKyR8rcE.d.ts +646 -0
- package/dist/chunk-42MGHABB.js +263 -0
- package/dist/chunk-42MGHABB.js.map +1 -0
- package/dist/chunk-7GOBAL4G.js +3 -0
- package/dist/chunk-7GOBAL4G.js.map +1 -0
- package/dist/chunk-G5I3T73A.js +152 -0
- package/dist/chunk-G5I3T73A.js.map +1 -0
- package/dist/chunk-IB4WUQDZ.js +410 -0
- package/dist/chunk-IB4WUQDZ.js.map +1 -0
- package/dist/chunk-MOG4Y6I7.js +415 -0
- package/dist/chunk-MOG4Y6I7.js.map +1 -0
- package/dist/chunk-NK4TJV2W.js +295 -0
- package/dist/chunk-NK4TJV2W.js.map +1 -0
- package/dist/chunk-RHNVRCF3.js +838 -0
- package/dist/chunk-RHNVRCF3.js.map +1 -0
- package/dist/chunk-YTCPXJR5.js +570 -0
- package/dist/chunk-YTCPXJR5.js.map +1 -0
- package/dist/cloudflare-kv-L64CZKDK.js +105 -0
- package/dist/cloudflare-kv-L64CZKDK.js.map +1 -0
- package/dist/deno-kv-F55HKKP6.js +111 -0
- package/dist/deno-kv-F55HKKP6.js.map +1 -0
- package/dist/index-C3kz9XqE.d.ts +226 -0
- package/dist/index-DOGcetyD.d.ts +1041 -0
- package/dist/index.d.ts +1579 -0
- package/dist/index.js +4294 -0
- package/dist/index.js.map +1 -0
- package/dist/jwt-manager-CH8H0kmm.d.ts +182 -0
- package/dist/providers/index.d.ts +90 -0
- package/dist/providers/index.js +3 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/otp/index.d.ts +3 -0
- package/dist/providers/otp/index.js +4 -0
- package/dist/providers/otp/index.js.map +1 -0
- package/dist/redis-5TIS6XCA.js +121 -0
- package/dist/redis-5TIS6XCA.js.map +1 -0
- package/dist/security/index.d.ts +301 -0
- package/dist/security/index.js +5 -0
- package/dist/security/index.js.map +1 -0
- package/dist/session/index.d.ts +117 -0
- package/dist/session/index.js +4 -0
- package/dist/session/index.js.map +1 -0
- package/dist/storage/index.d.ts +97 -0
- package/dist/storage/index.js +3 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/types-DSjafxJ4.d.ts +193 -0
- package/package.json +102 -0
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
import { StorageKeys } from './chunk-42MGHABB.js';
|
|
2
|
+
|
|
3
|
+
// src/security/rate-limiter.ts
|
|
4
|
+
var RateLimiter = class {
|
|
5
|
+
storage;
|
|
6
|
+
config;
|
|
7
|
+
constructor(storage, config) {
|
|
8
|
+
this.storage = storage;
|
|
9
|
+
this.config = {
|
|
10
|
+
keyPrefix: "",
|
|
11
|
+
...config
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Get storage key for rate limit entry
|
|
16
|
+
*/
|
|
17
|
+
getKey(identifier) {
|
|
18
|
+
return StorageKeys.rateLimit(`${this.config.keyPrefix}${identifier}`);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Check and consume rate limit
|
|
22
|
+
*/
|
|
23
|
+
async check(identifier) {
|
|
24
|
+
const key = this.getKey(identifier);
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
const windowMs = this.config.windowSeconds * 1e3;
|
|
27
|
+
const record = await this.storage.get(key);
|
|
28
|
+
let count;
|
|
29
|
+
let windowStart;
|
|
30
|
+
if (record) {
|
|
31
|
+
windowStart = new Date(record.windowStart).getTime();
|
|
32
|
+
const windowEnd = windowStart + windowMs;
|
|
33
|
+
if (now > windowEnd) {
|
|
34
|
+
count = 1;
|
|
35
|
+
windowStart = now;
|
|
36
|
+
} else {
|
|
37
|
+
count = record.count + 1;
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
count = 1;
|
|
41
|
+
windowStart = now;
|
|
42
|
+
}
|
|
43
|
+
const resetAt = new Date(windowStart + windowMs);
|
|
44
|
+
const remaining = Math.max(0, this.config.maxRequests - count);
|
|
45
|
+
const allowed = count <= this.config.maxRequests;
|
|
46
|
+
const ttl = Math.ceil((resetAt.getTime() - now) / 1e3);
|
|
47
|
+
if (ttl > 0) {
|
|
48
|
+
await this.storage.set(
|
|
49
|
+
key,
|
|
50
|
+
{
|
|
51
|
+
count,
|
|
52
|
+
windowStart: new Date(windowStart).toISOString()
|
|
53
|
+
},
|
|
54
|
+
ttl
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
allowed,
|
|
59
|
+
remaining,
|
|
60
|
+
resetAt,
|
|
61
|
+
retryAfterMs: allowed ? void 0 : resetAt.getTime() - now
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Get current rate limit status without consuming
|
|
66
|
+
*/
|
|
67
|
+
async status(identifier) {
|
|
68
|
+
const key = this.getKey(identifier);
|
|
69
|
+
const record = await this.storage.get(key);
|
|
70
|
+
if (!record) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
const windowMs = this.config.windowSeconds * 1e3;
|
|
75
|
+
const windowStart = new Date(record.windowStart).getTime();
|
|
76
|
+
const windowEnd = windowStart + windowMs;
|
|
77
|
+
if (now > windowEnd) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const remaining = Math.max(0, this.config.maxRequests - record.count);
|
|
81
|
+
const allowed = record.count < this.config.maxRequests;
|
|
82
|
+
return {
|
|
83
|
+
allowed,
|
|
84
|
+
remaining,
|
|
85
|
+
resetAt: new Date(windowEnd),
|
|
86
|
+
retryAfterMs: allowed ? void 0 : windowEnd - now
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Reset rate limit for an identifier
|
|
91
|
+
*/
|
|
92
|
+
async reset(identifier) {
|
|
93
|
+
const key = this.getKey(identifier);
|
|
94
|
+
await this.storage.delete(key);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Check rate limit without consuming (peek)
|
|
98
|
+
*/
|
|
99
|
+
async peek(identifier) {
|
|
100
|
+
const status = await this.status(identifier);
|
|
101
|
+
if (!status) {
|
|
102
|
+
return {
|
|
103
|
+
allowed: true,
|
|
104
|
+
remaining: this.config.maxRequests,
|
|
105
|
+
resetAt: new Date(Date.now() + this.config.windowSeconds * 1e3)
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return status;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
function createRateLimiter(storage, config) {
|
|
112
|
+
return new RateLimiter(storage, config);
|
|
113
|
+
}
|
|
114
|
+
var RateLimitPresets = {
|
|
115
|
+
/** Login attempts: 5 per 15 minutes */
|
|
116
|
+
login: {
|
|
117
|
+
windowSeconds: 15 * 60,
|
|
118
|
+
maxRequests: 5,
|
|
119
|
+
keyPrefix: "login:"
|
|
120
|
+
},
|
|
121
|
+
/** OTP requests: 5 per 15 minutes */
|
|
122
|
+
otp: {
|
|
123
|
+
windowSeconds: 15 * 60,
|
|
124
|
+
maxRequests: 5,
|
|
125
|
+
keyPrefix: "otp:"
|
|
126
|
+
},
|
|
127
|
+
/** Magic link requests: 3 per 10 minutes */
|
|
128
|
+
magicLink: {
|
|
129
|
+
windowSeconds: 10 * 60,
|
|
130
|
+
maxRequests: 3,
|
|
131
|
+
keyPrefix: "magic:"
|
|
132
|
+
},
|
|
133
|
+
/** Password reset: 3 per hour */
|
|
134
|
+
passwordReset: {
|
|
135
|
+
windowSeconds: 60 * 60,
|
|
136
|
+
maxRequests: 3,
|
|
137
|
+
keyPrefix: "pwreset:"
|
|
138
|
+
},
|
|
139
|
+
/** API general: 100 per minute */
|
|
140
|
+
api: {
|
|
141
|
+
windowSeconds: 60,
|
|
142
|
+
maxRequests: 100,
|
|
143
|
+
keyPrefix: "api:"
|
|
144
|
+
},
|
|
145
|
+
/** Registration: 5 per hour per IP */
|
|
146
|
+
registration: {
|
|
147
|
+
windowSeconds: 60 * 60,
|
|
148
|
+
maxRequests: 5,
|
|
149
|
+
keyPrefix: "register:"
|
|
150
|
+
},
|
|
151
|
+
/** 2FA attempts: 5 per 5 minutes */
|
|
152
|
+
twoFactor: {
|
|
153
|
+
windowSeconds: 5 * 60,
|
|
154
|
+
maxRequests: 5,
|
|
155
|
+
keyPrefix: "2fa:"
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// src/utils/crypto.ts
|
|
160
|
+
function generateRandomHex(byteLength = 32) {
|
|
161
|
+
const bytes = new Uint8Array(byteLength);
|
|
162
|
+
crypto.getRandomValues(bytes);
|
|
163
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
164
|
+
}
|
|
165
|
+
function generateRandomBase64Url(byteLength = 32) {
|
|
166
|
+
const bytes = new Uint8Array(byteLength);
|
|
167
|
+
crypto.getRandomValues(bytes);
|
|
168
|
+
return base64UrlEncode(bytes);
|
|
169
|
+
}
|
|
170
|
+
function randomInt(min, max) {
|
|
171
|
+
const range = max - min;
|
|
172
|
+
const bytesNeeded = Math.ceil(Math.log2(range) / 8) || 1;
|
|
173
|
+
const maxValid = Math.floor(256 ** bytesNeeded / range) * range;
|
|
174
|
+
let value;
|
|
175
|
+
const bytes = new Uint8Array(bytesNeeded);
|
|
176
|
+
do {
|
|
177
|
+
crypto.getRandomValues(bytes);
|
|
178
|
+
value = bytes.reduce((acc, byte, i) => acc + byte * 256 ** i, 0);
|
|
179
|
+
} while (value >= maxValid);
|
|
180
|
+
return min + value % range;
|
|
181
|
+
}
|
|
182
|
+
async function sha256Hex(data) {
|
|
183
|
+
const encoder = new TextEncoder();
|
|
184
|
+
const dataBuffer = encoder.encode(data);
|
|
185
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", dataBuffer);
|
|
186
|
+
return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
187
|
+
}
|
|
188
|
+
async function sha256(data) {
|
|
189
|
+
const encoder = new TextEncoder();
|
|
190
|
+
const dataBuffer = encoder.encode(data);
|
|
191
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", dataBuffer);
|
|
192
|
+
return new Uint8Array(hashBuffer);
|
|
193
|
+
}
|
|
194
|
+
function timingSafeEqual(a, b) {
|
|
195
|
+
if (a.length !== b.length) {
|
|
196
|
+
b = a;
|
|
197
|
+
}
|
|
198
|
+
const aBytes = new TextEncoder().encode(a);
|
|
199
|
+
const bBytes = new TextEncoder().encode(b);
|
|
200
|
+
let result = 0;
|
|
201
|
+
for (let i = 0; i < aBytes.length; i++) {
|
|
202
|
+
result |= aBytes[i] ^ bBytes[i];
|
|
203
|
+
}
|
|
204
|
+
return result === 0 && a.length === b.length;
|
|
205
|
+
}
|
|
206
|
+
function timingSafeEqualBytes(a, b) {
|
|
207
|
+
if (a.length !== b.length) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
let result = 0;
|
|
211
|
+
for (let i = 0; i < a.length; i++) {
|
|
212
|
+
result |= a[i] ^ b[i];
|
|
213
|
+
}
|
|
214
|
+
return result === 0;
|
|
215
|
+
}
|
|
216
|
+
function base64UrlEncode(data) {
|
|
217
|
+
const base64 = btoa(String.fromCharCode(...data));
|
|
218
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
219
|
+
}
|
|
220
|
+
function base64UrlDecode(str) {
|
|
221
|
+
const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
222
|
+
const paddedBase64 = base64 + "=".repeat((4 - base64.length % 4) % 4);
|
|
223
|
+
const binary = atob(paddedBase64);
|
|
224
|
+
return new Uint8Array([...binary].map((char) => char.charCodeAt(0)));
|
|
225
|
+
}
|
|
226
|
+
function hexToBytes(hex) {
|
|
227
|
+
const bytes = hex.match(/.{2}/g);
|
|
228
|
+
if (!bytes) return new Uint8Array(0);
|
|
229
|
+
return new Uint8Array(bytes.map((byte) => parseInt(byte, 16)));
|
|
230
|
+
}
|
|
231
|
+
function bytesToHex(bytes) {
|
|
232
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// src/security/csrf.ts
|
|
236
|
+
var CsrfManager = class {
|
|
237
|
+
storage;
|
|
238
|
+
config;
|
|
239
|
+
constructor(storage, config) {
|
|
240
|
+
this.storage = storage;
|
|
241
|
+
this.config = {
|
|
242
|
+
expiresIn: config?.expiresIn ?? 3600,
|
|
243
|
+
headerName: config?.headerName ?? "x-csrf-token",
|
|
244
|
+
cookieName: config?.cookieName ?? "csrf",
|
|
245
|
+
tokenLength: config?.tokenLength ?? 32
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Generate a cryptographically secure CSRF token
|
|
250
|
+
*/
|
|
251
|
+
generateToken() {
|
|
252
|
+
return generateRandomBase64Url(this.config.tokenLength);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Hash a CSRF token for storage
|
|
256
|
+
*/
|
|
257
|
+
async hashToken(token) {
|
|
258
|
+
return sha256Hex(token);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Generate token pair for double-submit pattern
|
|
262
|
+
*/
|
|
263
|
+
async generateTokenPair() {
|
|
264
|
+
const token = this.generateToken();
|
|
265
|
+
const hash = await this.hashToken(token);
|
|
266
|
+
return { token, hash };
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Store CSRF token for a session
|
|
270
|
+
*/
|
|
271
|
+
async storeToken(sessionId, token) {
|
|
272
|
+
const hash = await this.hashToken(token);
|
|
273
|
+
const key = StorageKeys.csrf(sessionId);
|
|
274
|
+
await this.storage.set(key, { hash, createdAt: (/* @__PURE__ */ new Date()).toISOString() }, this.config.expiresIn);
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Verify CSRF token against stored hash
|
|
278
|
+
*/
|
|
279
|
+
async verifyToken(token, hash) {
|
|
280
|
+
const computedHash = await this.hashToken(token);
|
|
281
|
+
return timingSafeEqual(computedHash, hash);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Verify CSRF token for a session
|
|
285
|
+
*/
|
|
286
|
+
async verifyTokenForSession(sessionId, token) {
|
|
287
|
+
const key = StorageKeys.csrf(sessionId);
|
|
288
|
+
const stored = await this.storage.get(key);
|
|
289
|
+
if (!stored) {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
return this.verifyToken(token, stored.hash);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Validate double-submit cookie pattern
|
|
296
|
+
* Compares CSRF token from header/body against cookie value
|
|
297
|
+
*/
|
|
298
|
+
validateDoubleSubmit(headerToken, cookieToken) {
|
|
299
|
+
if (!headerToken || !cookieToken) return false;
|
|
300
|
+
return timingSafeEqual(headerToken, cookieToken);
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Extract CSRF token from request headers or body
|
|
304
|
+
*/
|
|
305
|
+
extractTokenFromRequest(headers, body) {
|
|
306
|
+
const headerName = this.config.headerName.toLowerCase();
|
|
307
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
308
|
+
if (key.toLowerCase() === headerName && value) {
|
|
309
|
+
return value;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (body && typeof body["csrfToken"] === "string") {
|
|
313
|
+
return body["csrfToken"];
|
|
314
|
+
}
|
|
315
|
+
if (body && typeof body["_csrf"] === "string") {
|
|
316
|
+
return body["_csrf"];
|
|
317
|
+
}
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Delete CSRF token for a session
|
|
322
|
+
*/
|
|
323
|
+
async deleteToken(sessionId) {
|
|
324
|
+
const key = StorageKeys.csrf(sessionId);
|
|
325
|
+
await this.storage.delete(key);
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Refresh CSRF token for a session
|
|
329
|
+
*/
|
|
330
|
+
async refreshToken(sessionId) {
|
|
331
|
+
const pair = await this.generateTokenPair();
|
|
332
|
+
await this.storeToken(sessionId, pair.token);
|
|
333
|
+
return pair;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Get configuration
|
|
337
|
+
*/
|
|
338
|
+
getConfig() {
|
|
339
|
+
return { ...this.config };
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
function createCsrfManager(storage, config) {
|
|
343
|
+
return new CsrfManager(storage, config);
|
|
344
|
+
}
|
|
345
|
+
var CsrfUtils = {
|
|
346
|
+
/**
|
|
347
|
+
* Generate a CSRF token
|
|
348
|
+
*/
|
|
349
|
+
generateToken(length = 32) {
|
|
350
|
+
return generateRandomBase64Url(length);
|
|
351
|
+
},
|
|
352
|
+
/**
|
|
353
|
+
* Hash a token
|
|
354
|
+
*/
|
|
355
|
+
hashToken(token) {
|
|
356
|
+
return sha256Hex(token);
|
|
357
|
+
},
|
|
358
|
+
/**
|
|
359
|
+
* Generate token pair
|
|
360
|
+
*/
|
|
361
|
+
async generateTokenPair(length = 32) {
|
|
362
|
+
const token = generateRandomBase64Url(length);
|
|
363
|
+
const hash = await sha256Hex(token);
|
|
364
|
+
return { token, hash };
|
|
365
|
+
},
|
|
366
|
+
/**
|
|
367
|
+
* Verify token against hash
|
|
368
|
+
*/
|
|
369
|
+
async verifyToken(token, hash) {
|
|
370
|
+
const computedHash = await sha256Hex(token);
|
|
371
|
+
return timingSafeEqual(computedHash, hash);
|
|
372
|
+
},
|
|
373
|
+
/**
|
|
374
|
+
* Validate double-submit pattern
|
|
375
|
+
*/
|
|
376
|
+
validateDoubleSubmit(headerToken, cookieToken) {
|
|
377
|
+
if (!headerToken || !cookieToken) return false;
|
|
378
|
+
return timingSafeEqual(headerToken, cookieToken);
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
// src/security/lockout.ts
|
|
383
|
+
var LockoutManager = class {
|
|
384
|
+
storage;
|
|
385
|
+
config;
|
|
386
|
+
constructor(storage, config) {
|
|
387
|
+
this.storage = storage;
|
|
388
|
+
this.config = {
|
|
389
|
+
keyPrefix: "lockout:",
|
|
390
|
+
...config
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Get storage key for lockout entry
|
|
395
|
+
*/
|
|
396
|
+
getKey(identifier) {
|
|
397
|
+
return `${this.config.keyPrefix}${identifier}`;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Record a failed attempt
|
|
401
|
+
*/
|
|
402
|
+
async recordFailedAttempt(identifier) {
|
|
403
|
+
const key = this.getKey(identifier);
|
|
404
|
+
const now = Date.now();
|
|
405
|
+
const record = await this.storage.get(key);
|
|
406
|
+
let attempts;
|
|
407
|
+
let firstAttemptAt;
|
|
408
|
+
let lockedUntil;
|
|
409
|
+
if (record) {
|
|
410
|
+
if (record.lockedUntil) {
|
|
411
|
+
const lockedUntilTime = new Date(record.lockedUntil).getTime();
|
|
412
|
+
if (now < lockedUntilTime) {
|
|
413
|
+
return {
|
|
414
|
+
locked: true,
|
|
415
|
+
attempts: record.attempts,
|
|
416
|
+
remainingAttempts: 0,
|
|
417
|
+
unlocksAt: new Date(lockedUntilTime),
|
|
418
|
+
unlocksInSeconds: Math.ceil((lockedUntilTime - now) / 1e3)
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
attempts = 1;
|
|
422
|
+
firstAttemptAt = now;
|
|
423
|
+
lockedUntil = void 0;
|
|
424
|
+
} else {
|
|
425
|
+
firstAttemptAt = new Date(record.firstAttemptAt).getTime();
|
|
426
|
+
const windowEnd2 = firstAttemptAt + this.config.attemptWindow * 1e3;
|
|
427
|
+
if (now > windowEnd2) {
|
|
428
|
+
attempts = 1;
|
|
429
|
+
firstAttemptAt = now;
|
|
430
|
+
} else {
|
|
431
|
+
attempts = record.attempts + 1;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
} else {
|
|
435
|
+
attempts = 1;
|
|
436
|
+
firstAttemptAt = now;
|
|
437
|
+
}
|
|
438
|
+
if (attempts >= this.config.maxAttempts) {
|
|
439
|
+
lockedUntil = now + this.config.lockoutDuration * 1e3;
|
|
440
|
+
}
|
|
441
|
+
const windowEnd = firstAttemptAt + this.config.attemptWindow * 1e3;
|
|
442
|
+
const ttlEnd = lockedUntil ?? windowEnd;
|
|
443
|
+
const ttl = Math.ceil((ttlEnd - now) / 1e3);
|
|
444
|
+
if (ttl > 0) {
|
|
445
|
+
await this.storage.set(
|
|
446
|
+
key,
|
|
447
|
+
{
|
|
448
|
+
attempts,
|
|
449
|
+
firstAttemptAt: new Date(firstAttemptAt).toISOString(),
|
|
450
|
+
lockedUntil: lockedUntil ? new Date(lockedUntil).toISOString() : void 0
|
|
451
|
+
},
|
|
452
|
+
ttl
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
const remainingAttempts = Math.max(0, this.config.maxAttempts - attempts);
|
|
456
|
+
if (lockedUntil) {
|
|
457
|
+
return {
|
|
458
|
+
locked: true,
|
|
459
|
+
attempts,
|
|
460
|
+
remainingAttempts: 0,
|
|
461
|
+
unlocksAt: new Date(lockedUntil),
|
|
462
|
+
unlocksInSeconds: Math.ceil((lockedUntil - now) / 1e3)
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
return {
|
|
466
|
+
locked: false,
|
|
467
|
+
attempts,
|
|
468
|
+
remainingAttempts
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Record a successful attempt (clear lockout state)
|
|
473
|
+
*/
|
|
474
|
+
async recordSuccessfulAttempt(identifier) {
|
|
475
|
+
const key = this.getKey(identifier);
|
|
476
|
+
await this.storage.delete(key);
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Get current lockout status
|
|
480
|
+
*/
|
|
481
|
+
async getStatus(identifier) {
|
|
482
|
+
const key = this.getKey(identifier);
|
|
483
|
+
const record = await this.storage.get(key);
|
|
484
|
+
const now = Date.now();
|
|
485
|
+
if (!record) {
|
|
486
|
+
return {
|
|
487
|
+
locked: false,
|
|
488
|
+
attempts: 0,
|
|
489
|
+
remainingAttempts: this.config.maxAttempts
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
if (record.lockedUntil) {
|
|
493
|
+
const lockedUntilTime = new Date(record.lockedUntil).getTime();
|
|
494
|
+
if (now < lockedUntilTime) {
|
|
495
|
+
return {
|
|
496
|
+
locked: true,
|
|
497
|
+
attempts: record.attempts,
|
|
498
|
+
remainingAttempts: 0,
|
|
499
|
+
unlocksAt: new Date(lockedUntilTime),
|
|
500
|
+
unlocksInSeconds: Math.ceil((lockedUntilTime - now) / 1e3)
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
return {
|
|
504
|
+
locked: false,
|
|
505
|
+
attempts: 0,
|
|
506
|
+
remainingAttempts: this.config.maxAttempts
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
const firstAttemptAt = new Date(record.firstAttemptAt).getTime();
|
|
510
|
+
const windowEnd = firstAttemptAt + this.config.attemptWindow * 1e3;
|
|
511
|
+
if (now > windowEnd) {
|
|
512
|
+
return {
|
|
513
|
+
locked: false,
|
|
514
|
+
attempts: 0,
|
|
515
|
+
remainingAttempts: this.config.maxAttempts
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
return {
|
|
519
|
+
locked: false,
|
|
520
|
+
attempts: record.attempts,
|
|
521
|
+
remainingAttempts: Math.max(0, this.config.maxAttempts - record.attempts)
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Check if account is locked
|
|
526
|
+
*/
|
|
527
|
+
async isLocked(identifier) {
|
|
528
|
+
const status = await this.getStatus(identifier);
|
|
529
|
+
return status.locked;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Manually lock an account
|
|
533
|
+
*/
|
|
534
|
+
async lock(identifier, durationSeconds) {
|
|
535
|
+
const key = this.getKey(identifier);
|
|
536
|
+
const duration = durationSeconds ?? this.config.lockoutDuration;
|
|
537
|
+
const lockedUntil = new Date(Date.now() + duration * 1e3);
|
|
538
|
+
await this.storage.set(
|
|
539
|
+
key,
|
|
540
|
+
{
|
|
541
|
+
attempts: this.config.maxAttempts,
|
|
542
|
+
firstAttemptAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
543
|
+
lockedUntil: lockedUntil.toISOString()
|
|
544
|
+
},
|
|
545
|
+
duration
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Manually unlock an account
|
|
550
|
+
*/
|
|
551
|
+
async unlock(identifier) {
|
|
552
|
+
const key = this.getKey(identifier);
|
|
553
|
+
await this.storage.delete(key);
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
function createLockoutManager(storage, config) {
|
|
557
|
+
return new LockoutManager(storage, config);
|
|
558
|
+
}
|
|
559
|
+
var DefaultLockoutConfig = {
|
|
560
|
+
maxAttempts: 5,
|
|
561
|
+
lockoutDuration: 15 * 60,
|
|
562
|
+
// 15 minutes
|
|
563
|
+
attemptWindow: 15 * 60,
|
|
564
|
+
// 15 minutes
|
|
565
|
+
keyPrefix: "lockout:"
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
export { CsrfManager, CsrfUtils, DefaultLockoutConfig, LockoutManager, RateLimitPresets, RateLimiter, base64UrlDecode, base64UrlEncode, bytesToHex, createCsrfManager, createLockoutManager, createRateLimiter, generateRandomBase64Url, generateRandomHex, hexToBytes, randomInt, sha256, sha256Hex, timingSafeEqual, timingSafeEqualBytes };
|
|
569
|
+
//# sourceMappingURL=chunk-YTCPXJR5.js.map
|
|
570
|
+
//# sourceMappingURL=chunk-YTCPXJR5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/security/rate-limiter.ts","../src/utils/crypto.ts","../src/security/csrf.ts","../src/security/lockout.ts"],"names":["windowEnd"],"mappings":"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","file":"chunk-YTCPXJR5.js","sourcesContent":["/**\n * Rate Limiting\n * Uses KVStorage interface for multi-runtime support\n */\n\nimport type { KVStorage } from '../storage/types.js';\nimport { StorageKeys } from '../storage/index.js';\n\n/**\n * Rate limit configuration\n */\nexport interface RateLimitConfig {\n /** Time window in seconds */\n windowSeconds: number;\n /** Max requests per window */\n maxRequests: number;\n /** Key prefix for rate limit entries */\n keyPrefix?: string;\n}\n\n/**\n * Rate limit result\n */\nexport interface RateLimitResult {\n /** Whether the request is allowed */\n allowed: boolean;\n /** Remaining requests in current window */\n remaining: number;\n /** When the window resets */\n resetAt: Date;\n /** Milliseconds until retry is allowed (if not allowed) */\n retryAfterMs?: number;\n}\n\n/**\n * Rate limit record stored in KV\n */\ninterface RateLimitRecord {\n count: number;\n windowStart: string;\n}\n\n/**\n * Rate Limiter using KVStorage\n */\nexport class RateLimiter {\n private storage: KVStorage;\n private config: Required<RateLimitConfig>;\n\n constructor(storage: KVStorage, config: RateLimitConfig) {\n this.storage = storage;\n this.config = {\n keyPrefix: '',\n ...config,\n };\n }\n\n /**\n * Get storage key for rate limit entry\n */\n private getKey(identifier: string): string {\n return StorageKeys.rateLimit(`${this.config.keyPrefix}${identifier}`);\n }\n\n /**\n * Check and consume rate limit\n */\n async check(identifier: string): Promise<RateLimitResult> {\n const key = this.getKey(identifier);\n const now = Date.now();\n const windowMs = this.config.windowSeconds * 1000;\n\n // Get existing record\n const record = await this.storage.get<RateLimitRecord>(key);\n\n let count: number;\n let windowStart: number;\n\n if (record) {\n windowStart = new Date(record.windowStart).getTime();\n const windowEnd = windowStart + windowMs;\n\n if (now > windowEnd) {\n // Window expired, start new one\n count = 1;\n windowStart = now;\n } else {\n // Increment within window\n count = record.count + 1;\n }\n } else {\n // No record, start new window\n count = 1;\n windowStart = now;\n }\n\n const resetAt = new Date(windowStart + windowMs);\n const remaining = Math.max(0, this.config.maxRequests - count);\n const allowed = count <= this.config.maxRequests;\n\n // Update storage\n const ttl = Math.ceil((resetAt.getTime() - now) / 1000);\n if (ttl > 0) {\n await this.storage.set<RateLimitRecord>(\n key,\n {\n count,\n windowStart: new Date(windowStart).toISOString(),\n },\n ttl\n );\n }\n\n return {\n allowed,\n remaining,\n resetAt,\n retryAfterMs: allowed ? undefined : resetAt.getTime() - now,\n };\n }\n\n /**\n * Get current rate limit status without consuming\n */\n async status(identifier: string): Promise<RateLimitResult | null> {\n const key = this.getKey(identifier);\n const record = await this.storage.get<RateLimitRecord>(key);\n\n if (!record) {\n return null;\n }\n\n const now = Date.now();\n const windowMs = this.config.windowSeconds * 1000;\n const windowStart = new Date(record.windowStart).getTime();\n const windowEnd = windowStart + windowMs;\n\n if (now > windowEnd) {\n return null; // Window expired\n }\n\n const remaining = Math.max(0, this.config.maxRequests - record.count);\n const allowed = record.count < this.config.maxRequests;\n\n return {\n allowed,\n remaining,\n resetAt: new Date(windowEnd),\n retryAfterMs: allowed ? undefined : windowEnd - now,\n };\n }\n\n /**\n * Reset rate limit for an identifier\n */\n async reset(identifier: string): Promise<void> {\n const key = this.getKey(identifier);\n await this.storage.delete(key);\n }\n\n /**\n * Check rate limit without consuming (peek)\n */\n async peek(identifier: string): Promise<RateLimitResult> {\n const status = await this.status(identifier);\n if (!status) {\n return {\n allowed: true,\n remaining: this.config.maxRequests,\n resetAt: new Date(Date.now() + this.config.windowSeconds * 1000),\n };\n }\n return status;\n }\n}\n\n/**\n * Create a rate limiter\n */\nexport function createRateLimiter(\n storage: KVStorage,\n config: RateLimitConfig\n): RateLimiter {\n return new RateLimiter(storage, config);\n}\n\n/**\n * Common rate limit configurations\n */\nexport const RateLimitPresets = {\n /** Login attempts: 5 per 15 minutes */\n login: {\n windowSeconds: 15 * 60,\n maxRequests: 5,\n keyPrefix: 'login:',\n },\n\n /** OTP requests: 5 per 15 minutes */\n otp: {\n windowSeconds: 15 * 60,\n maxRequests: 5,\n keyPrefix: 'otp:',\n },\n\n /** Magic link requests: 3 per 10 minutes */\n magicLink: {\n windowSeconds: 10 * 60,\n maxRequests: 3,\n keyPrefix: 'magic:',\n },\n\n /** Password reset: 3 per hour */\n passwordReset: {\n windowSeconds: 60 * 60,\n maxRequests: 3,\n keyPrefix: 'pwreset:',\n },\n\n /** API general: 100 per minute */\n api: {\n windowSeconds: 60,\n maxRequests: 100,\n keyPrefix: 'api:',\n },\n\n /** Registration: 5 per hour per IP */\n registration: {\n windowSeconds: 60 * 60,\n maxRequests: 5,\n keyPrefix: 'register:',\n },\n\n /** 2FA attempts: 5 per 5 minutes */\n twoFactor: {\n windowSeconds: 5 * 60,\n maxRequests: 5,\n keyPrefix: '2fa:',\n },\n} as const;\n","/**\n * Runtime-agnostic Crypto Utilities\n * Uses Web Crypto API (works in Node.js, Deno, Bun, Cloudflare Workers)\n */\n\n/**\n * Generate cryptographically secure random bytes as hex string\n */\nexport function generateRandomHex(byteLength = 32): string {\n const bytes = new Uint8Array(byteLength);\n crypto.getRandomValues(bytes);\n return Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n}\n\n/**\n * Generate cryptographically secure random bytes as base64url string\n */\nexport function generateRandomBase64Url(byteLength = 32): string {\n const bytes = new Uint8Array(byteLength);\n crypto.getRandomValues(bytes);\n return base64UrlEncode(bytes);\n}\n\n/**\n * Generate a random integer between min (inclusive) and max (exclusive)\n * Uses rejection sampling for uniform distribution\n */\nexport function randomInt(min: number, max: number): number {\n const range = max - min;\n const bytesNeeded = Math.ceil(Math.log2(range) / 8) || 1;\n const maxValid = Math.floor(256 ** bytesNeeded / range) * range;\n\n let value: number;\n const bytes = new Uint8Array(bytesNeeded);\n\n do {\n crypto.getRandomValues(bytes);\n value = bytes.reduce((acc, byte, i) => acc + byte * 256 ** i, 0);\n } while (value >= maxValid);\n\n return min + (value % range);\n}\n\n/**\n * Hash data using SHA-256 and return as hex string\n */\nexport async function sha256Hex(data: string): Promise<string> {\n const encoder = new TextEncoder();\n const dataBuffer = encoder.encode(data);\n const hashBuffer = await crypto.subtle.digest('SHA-256', dataBuffer);\n return Array.from(new Uint8Array(hashBuffer))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n}\n\n/**\n * Hash data using SHA-256 and return as Uint8Array\n */\nexport async function sha256(data: string): Promise<Uint8Array> {\n const encoder = new TextEncoder();\n const dataBuffer = encoder.encode(data);\n const hashBuffer = await crypto.subtle.digest('SHA-256', dataBuffer);\n return new Uint8Array(hashBuffer);\n}\n\n/**\n * Timing-safe comparison of two strings\n * Prevents timing attacks by always comparing all bytes\n */\nexport function timingSafeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) {\n // Still do a comparison to maintain constant time\n b = a;\n }\n\n const aBytes = new TextEncoder().encode(a);\n const bBytes = new TextEncoder().encode(b);\n\n let result = 0;\n for (let i = 0; i < aBytes.length; i++) {\n result |= aBytes[i]! ^ bBytes[i]!;\n }\n\n return result === 0 && a.length === b.length;\n}\n\n/**\n * Timing-safe comparison of two Uint8Arrays\n */\nexport function timingSafeEqualBytes(a: Uint8Array, b: Uint8Array): boolean {\n if (a.length !== b.length) {\n return false;\n }\n\n let result = 0;\n for (let i = 0; i < a.length; i++) {\n result |= a[i]! ^ b[i]!;\n }\n\n return result === 0;\n}\n\n/**\n * Derive a key using PBKDF2\n */\nexport async function deriveKey(\n password: string,\n salt: Uint8Array,\n iterations = 100000,\n keyLength = 32\n): Promise<Uint8Array> {\n const encoder = new TextEncoder();\n const passwordKey = await crypto.subtle.importKey(\n 'raw',\n encoder.encode(password),\n 'PBKDF2',\n false,\n ['deriveBits']\n );\n\n const derivedBits = await crypto.subtle.deriveBits(\n {\n name: 'PBKDF2',\n salt: salt.buffer as ArrayBuffer,\n iterations,\n hash: 'SHA-256',\n },\n passwordKey,\n keyLength * 8\n );\n\n return new Uint8Array(derivedBits);\n}\n\n/**\n * Base64URL encode (URL-safe base64 without padding)\n */\nexport function base64UrlEncode(data: Uint8Array): string {\n const base64 = btoa(String.fromCharCode(...data));\n return base64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\n/**\n * Base64URL decode\n */\nexport function base64UrlDecode(str: string): Uint8Array {\n const base64 = str.replace(/-/g, '+').replace(/_/g, '/');\n const paddedBase64 = base64 + '='.repeat((4 - (base64.length % 4)) % 4);\n const binary = atob(paddedBase64);\n return new Uint8Array([...binary].map((char) => char.charCodeAt(0)));\n}\n\n/**\n * Hex string to Uint8Array\n */\nexport function hexToBytes(hex: string): Uint8Array {\n const bytes = hex.match(/.{2}/g);\n if (!bytes) return new Uint8Array(0);\n return new Uint8Array(bytes.map((byte) => parseInt(byte, 16)));\n}\n\n/**\n * Uint8Array to hex string\n */\nexport function bytesToHex(bytes: Uint8Array): string {\n return Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n}\n","/**\n * CSRF Protection\n * Double-Submit Cookie Pattern with KVStorage support\n */\n\nimport type { KVStorage } from '../storage/types.js';\nimport { StorageKeys } from '../storage/index.js';\nimport {\n generateRandomBase64Url,\n sha256Hex,\n timingSafeEqual,\n} from '../utils/crypto.js';\n\n/**\n * CSRF configuration\n */\nexport interface CsrfConfig {\n /** Token expiry in seconds (default: 3600 = 1 hour) */\n expiresIn?: number;\n /** Header name for CSRF token (default: 'x-csrf-token') */\n headerName?: string;\n /** Cookie name for CSRF token (default: 'csrf') */\n cookieName?: string;\n /** Token byte length (default: 32) */\n tokenLength?: number;\n}\n\n/**\n * CSRF token pair\n */\nexport interface CsrfTokenPair {\n /** Plain token (for client) */\n token: string;\n /** Hashed token (for server storage/cookie) */\n hash: string;\n}\n\n/**\n * CSRF Manager\n */\nexport class CsrfManager {\n private storage: KVStorage;\n private config: Required<CsrfConfig>;\n\n constructor(storage: KVStorage, config?: CsrfConfig) {\n this.storage = storage;\n this.config = {\n expiresIn: config?.expiresIn ?? 3600,\n headerName: config?.headerName ?? 'x-csrf-token',\n cookieName: config?.cookieName ?? 'csrf',\n tokenLength: config?.tokenLength ?? 32,\n };\n }\n\n /**\n * Generate a cryptographically secure CSRF token\n */\n generateToken(): string {\n return generateRandomBase64Url(this.config.tokenLength);\n }\n\n /**\n * Hash a CSRF token for storage\n */\n async hashToken(token: string): Promise<string> {\n return sha256Hex(token);\n }\n\n /**\n * Generate token pair for double-submit pattern\n */\n async generateTokenPair(): Promise<CsrfTokenPair> {\n const token = this.generateToken();\n const hash = await this.hashToken(token);\n return { token, hash };\n }\n\n /**\n * Store CSRF token for a session\n */\n async storeToken(sessionId: string, token: string): Promise<void> {\n const hash = await this.hashToken(token);\n const key = StorageKeys.csrf(sessionId);\n await this.storage.set(key, { hash, createdAt: new Date().toISOString() }, this.config.expiresIn);\n }\n\n /**\n * Verify CSRF token against stored hash\n */\n async verifyToken(token: string, hash: string): Promise<boolean> {\n const computedHash = await this.hashToken(token);\n return timingSafeEqual(computedHash, hash);\n }\n\n /**\n * Verify CSRF token for a session\n */\n async verifyTokenForSession(sessionId: string, token: string): Promise<boolean> {\n const key = StorageKeys.csrf(sessionId);\n const stored = await this.storage.get<{ hash: string }>(key);\n\n if (!stored) {\n return false;\n }\n\n return this.verifyToken(token, stored.hash);\n }\n\n /**\n * Validate double-submit cookie pattern\n * Compares CSRF token from header/body against cookie value\n */\n validateDoubleSubmit(headerToken: string, cookieToken: string): boolean {\n if (!headerToken || !cookieToken) return false;\n return timingSafeEqual(headerToken, cookieToken);\n }\n\n /**\n * Extract CSRF token from request headers or body\n */\n extractTokenFromRequest(\n headers: Record<string, string | undefined>,\n body?: Record<string, unknown>\n ): string | null {\n // Check header first (case-insensitive)\n const headerName = this.config.headerName.toLowerCase();\n for (const [key, value] of Object.entries(headers)) {\n if (key.toLowerCase() === headerName && value) {\n return value;\n }\n }\n\n // Check request body\n if (body && typeof body['csrfToken'] === 'string') {\n return body['csrfToken'];\n }\n if (body && typeof body['_csrf'] === 'string') {\n return body['_csrf'];\n }\n\n return null;\n }\n\n /**\n * Delete CSRF token for a session\n */\n async deleteToken(sessionId: string): Promise<void> {\n const key = StorageKeys.csrf(sessionId);\n await this.storage.delete(key);\n }\n\n /**\n * Refresh CSRF token for a session\n */\n async refreshToken(sessionId: string): Promise<CsrfTokenPair> {\n const pair = await this.generateTokenPair();\n await this.storeToken(sessionId, pair.token);\n return pair;\n }\n\n /**\n * Get configuration\n */\n getConfig(): Required<CsrfConfig> {\n return { ...this.config };\n }\n}\n\n/**\n * Create CSRF manager\n */\nexport function createCsrfManager(\n storage: KVStorage,\n config?: CsrfConfig\n): CsrfManager {\n return new CsrfManager(storage, config);\n}\n\n/**\n * Static CSRF utilities (for stateless double-submit pattern)\n */\nexport const CsrfUtils = {\n /**\n * Generate a CSRF token\n */\n generateToken(length = 32): string {\n return generateRandomBase64Url(length);\n },\n\n /**\n * Hash a token\n */\n hashToken(token: string): Promise<string> {\n return sha256Hex(token);\n },\n\n /**\n * Generate token pair\n */\n async generateTokenPair(length = 32): Promise<CsrfTokenPair> {\n const token = generateRandomBase64Url(length);\n const hash = await sha256Hex(token);\n return { token, hash };\n },\n\n /**\n * Verify token against hash\n */\n async verifyToken(token: string, hash: string): Promise<boolean> {\n const computedHash = await sha256Hex(token);\n return timingSafeEqual(computedHash, hash);\n },\n\n /**\n * Validate double-submit pattern\n */\n validateDoubleSubmit(headerToken: string, cookieToken: string): boolean {\n if (!headerToken || !cookieToken) return false;\n return timingSafeEqual(headerToken, cookieToken);\n },\n};\n","/**\n * Account Lockout\n * Prevents brute force attacks by locking accounts after failed attempts\n */\n\nimport type { KVStorage } from '../storage/types.js';\n\n/**\n * Lockout configuration\n */\nexport interface LockoutConfig {\n /** Maximum failed attempts before lockout */\n maxAttempts: number;\n /** Lockout duration in seconds */\n lockoutDuration: number;\n /** Time window for counting attempts in seconds */\n attemptWindow: number;\n /** Key prefix for lockout entries */\n keyPrefix?: string;\n}\n\n/**\n * Lockout status\n */\nexport interface LockoutStatus {\n /** Whether the account is locked */\n locked: boolean;\n /** Number of failed attempts */\n attempts: number;\n /** Remaining attempts before lockout */\n remainingAttempts: number;\n /** When the lockout expires (if locked) */\n unlocksAt?: Date;\n /** Seconds until unlock (if locked) */\n unlocksInSeconds?: number;\n}\n\n/**\n * Lockout record stored in KV\n */\ninterface LockoutRecord {\n attempts: number;\n firstAttemptAt: string;\n lockedUntil?: string;\n}\n\n/**\n * Account Lockout Manager\n */\nexport class LockoutManager {\n private storage: KVStorage;\n private config: Required<LockoutConfig>;\n\n constructor(storage: KVStorage, config: LockoutConfig) {\n this.storage = storage;\n this.config = {\n keyPrefix: 'lockout:',\n ...config,\n };\n }\n\n /**\n * Get storage key for lockout entry\n */\n private getKey(identifier: string): string {\n return `${this.config.keyPrefix}${identifier}`;\n }\n\n /**\n * Record a failed attempt\n */\n async recordFailedAttempt(identifier: string): Promise<LockoutStatus> {\n const key = this.getKey(identifier);\n const now = Date.now();\n const record = await this.storage.get<LockoutRecord>(key);\n\n let attempts: number;\n let firstAttemptAt: number;\n let lockedUntil: number | undefined;\n\n if (record) {\n // Check if currently locked\n if (record.lockedUntil) {\n const lockedUntilTime = new Date(record.lockedUntil).getTime();\n if (now < lockedUntilTime) {\n // Still locked\n return {\n locked: true,\n attempts: record.attempts,\n remainingAttempts: 0,\n unlocksAt: new Date(lockedUntilTime),\n unlocksInSeconds: Math.ceil((lockedUntilTime - now) / 1000),\n };\n }\n // Lock expired, reset\n attempts = 1;\n firstAttemptAt = now;\n lockedUntil = undefined;\n } else {\n // Check if attempt window expired\n firstAttemptAt = new Date(record.firstAttemptAt).getTime();\n const windowEnd = firstAttemptAt + this.config.attemptWindow * 1000;\n\n if (now > windowEnd) {\n // Window expired, reset\n attempts = 1;\n firstAttemptAt = now;\n } else {\n // Increment attempts\n attempts = record.attempts + 1;\n }\n }\n } else {\n // First attempt\n attempts = 1;\n firstAttemptAt = now;\n }\n\n // Check if should lock\n if (attempts >= this.config.maxAttempts) {\n lockedUntil = now + this.config.lockoutDuration * 1000;\n }\n\n // Calculate TTL - whichever is longer: window or lockout\n const windowEnd = firstAttemptAt + this.config.attemptWindow * 1000;\n const ttlEnd = lockedUntil ?? windowEnd;\n const ttl = Math.ceil((ttlEnd - now) / 1000);\n\n // Store record\n if (ttl > 0) {\n await this.storage.set<LockoutRecord>(\n key,\n {\n attempts,\n firstAttemptAt: new Date(firstAttemptAt).toISOString(),\n lockedUntil: lockedUntil ? new Date(lockedUntil).toISOString() : undefined,\n },\n ttl\n );\n }\n\n const remainingAttempts = Math.max(0, this.config.maxAttempts - attempts);\n\n if (lockedUntil) {\n return {\n locked: true,\n attempts,\n remainingAttempts: 0,\n unlocksAt: new Date(lockedUntil),\n unlocksInSeconds: Math.ceil((lockedUntil - now) / 1000),\n };\n }\n\n return {\n locked: false,\n attempts,\n remainingAttempts,\n };\n }\n\n /**\n * Record a successful attempt (clear lockout state)\n */\n async recordSuccessfulAttempt(identifier: string): Promise<void> {\n const key = this.getKey(identifier);\n await this.storage.delete(key);\n }\n\n /**\n * Get current lockout status\n */\n async getStatus(identifier: string): Promise<LockoutStatus> {\n const key = this.getKey(identifier);\n const record = await this.storage.get<LockoutRecord>(key);\n const now = Date.now();\n\n if (!record) {\n return {\n locked: false,\n attempts: 0,\n remainingAttempts: this.config.maxAttempts,\n };\n }\n\n // Check if locked\n if (record.lockedUntil) {\n const lockedUntilTime = new Date(record.lockedUntil).getTime();\n if (now < lockedUntilTime) {\n return {\n locked: true,\n attempts: record.attempts,\n remainingAttempts: 0,\n unlocksAt: new Date(lockedUntilTime),\n unlocksInSeconds: Math.ceil((lockedUntilTime - now) / 1000),\n };\n }\n // Lock expired\n return {\n locked: false,\n attempts: 0,\n remainingAttempts: this.config.maxAttempts,\n };\n }\n\n // Check if window expired\n const firstAttemptAt = new Date(record.firstAttemptAt).getTime();\n const windowEnd = firstAttemptAt + this.config.attemptWindow * 1000;\n\n if (now > windowEnd) {\n return {\n locked: false,\n attempts: 0,\n remainingAttempts: this.config.maxAttempts,\n };\n }\n\n return {\n locked: false,\n attempts: record.attempts,\n remainingAttempts: Math.max(0, this.config.maxAttempts - record.attempts),\n };\n }\n\n /**\n * Check if account is locked\n */\n async isLocked(identifier: string): Promise<boolean> {\n const status = await this.getStatus(identifier);\n return status.locked;\n }\n\n /**\n * Manually lock an account\n */\n async lock(identifier: string, durationSeconds?: number): Promise<void> {\n const key = this.getKey(identifier);\n const duration = durationSeconds ?? this.config.lockoutDuration;\n const lockedUntil = new Date(Date.now() + duration * 1000);\n\n await this.storage.set<LockoutRecord>(\n key,\n {\n attempts: this.config.maxAttempts,\n firstAttemptAt: new Date().toISOString(),\n lockedUntil: lockedUntil.toISOString(),\n },\n duration\n );\n }\n\n /**\n * Manually unlock an account\n */\n async unlock(identifier: string): Promise<void> {\n const key = this.getKey(identifier);\n await this.storage.delete(key);\n }\n}\n\n/**\n * Create lockout manager\n */\nexport function createLockoutManager(\n storage: KVStorage,\n config: LockoutConfig\n): LockoutManager {\n return new LockoutManager(storage, config);\n}\n\n/**\n * Default lockout configuration\n */\nexport const DefaultLockoutConfig: LockoutConfig = {\n maxAttempts: 5,\n lockoutDuration: 15 * 60, // 15 minutes\n attemptWindow: 15 * 60, // 15 minutes\n keyPrefix: 'lockout:',\n};\n"]}
|