k9guard 1.0.3 → 1.0.4
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 +196 -5
- package/dist/index.cjs +1555 -0
- package/dist/index.d.cts +141 -0
- package/dist/index.d.ts +141 -0
- package/dist/index.js +1526 -0
- package/docs/tr/README.md +196 -5
- package/package.json +35 -13
- package/index.ts +0 -4
- package/src/K9Guard.ts +0 -97
- package/src/core/captchaGenerator.ts +0 -372
- package/src/core/captchaValidator.ts +0 -198
- package/src/types/index.ts +0 -84
- package/src/utils/crypto.ts +0 -336
- package/src/utils/customQuestionGenerator.ts +0 -40
- package/src/utils/emojiGenerator.ts +0 -88
- package/src/utils/imageGenerator.ts +0 -154
- package/src/utils/random.ts +0 -43
- package/src/utils/reverseGenerator.ts +0 -54
- package/src/utils/scrambleGenerator.ts +0 -49
- package/src/utils/sequenceGenerator.ts +0 -34
- package/src/validators/customQuestionValidator.ts +0 -88
- package/tsconfig.json +0 -29
package/docs/tr/README.md
CHANGED
|
@@ -10,13 +10,14 @@ TypeScript/JavaScript projeleri için kriptografik güvenlik sunan güvenli, haf
|
|
|
10
10
|
|
|
11
11
|
- **Kriptografik Güvenlik**: NIST SP 800-90A standardına uyumluluk sağlanmıştır
|
|
12
12
|
- **10 CAPTCHA Türü**: Matematik, metin, dizi, karıştırma, ters çevirme, karma, çok adımlı, görsel, emoji ve özel doğrulama yöntemleri
|
|
13
|
+
- **Adaptif Zorluk**: Kullanıcı başarı oranına göre zorluk seviyesini otomatik ayarlar
|
|
13
14
|
- **Güvenlik Odaklı**: SHA-256 tuzlu hash algoritması, sunucu taraflı challenge deposu, nonce tabanlı oturum yönetimi ve 5 dakikalık geçerlilik süresi
|
|
14
15
|
- **Tek Kullanımlık Challenge**: Her nonce, `validate()` çağrısında — doğru ya da yanlış fark etmeksizin — tüketilir; replay ve brute-force saldırıları engellenir
|
|
15
16
|
- **Katı Yapılandırma**: Geçersiz `type` veya `difficulty` değerleri anında hata fırlatır; sessiz fallback yoktur
|
|
16
17
|
- **Girdi Doğrulama**: Enjeksiyon saldırılarını önlemek için uzunluk sınırlamaları, katı sayısal ayrıştırma, tip kontrolü ve sanitizasyon
|
|
17
18
|
- **Özel Sorular**: Doğrulama ve sanitizasyon ile kendi sorularınızı tanımlama desteği
|
|
18
19
|
- **Sıfır Bağımlılık**: Harici bağımlılık gerektirmeyen hafif yapı
|
|
19
|
-
- **Kapsamlı Test Edilmiş**:
|
|
20
|
+
- **Kapsamlı Test Edilmiş**: 228+ test ile birim, entegrasyon, güvenlik, uç durum ve benchmark senaryoları
|
|
20
21
|
- **OWASP Uyumlu**: OWASP Top 10 güvenlik yönergelerine uygun geliştirme
|
|
21
22
|
- **Gizlilik Uyumlu**: Kişisel veri saklamayan GDPR/KVKK uyumlu mimari
|
|
22
23
|
|
|
@@ -192,6 +193,101 @@ const challenge = captcha.generate();
|
|
|
192
193
|
const isValid = captcha.validate(challenge, "ankara");
|
|
193
194
|
```
|
|
194
195
|
|
|
196
|
+
### Adaptif Zorluk
|
|
197
|
+
|
|
198
|
+
Adaptif zorluk, kullanıcının başarı oranına göre zorluk seviyesini otomatik olarak ayarlar. Bu sayede zorlanan kullanıcılara kolay, hızlı çözen kullanıcılara ise daha zor challenge'lar sunulur.
|
|
199
|
+
|
|
200
|
+
#### Nasıl Çalışır?
|
|
201
|
+
|
|
202
|
+
- Oturum başına son 10 denemeyi takip eder (kayan pencere)
|
|
203
|
+
- **%80+ başarı oranı** — zorluk artar (easy -> medium -> hard)
|
|
204
|
+
- **%40 ve altı başarı oranı** — zorluk düşer (hard -> medium -> easy)
|
|
205
|
+
- **%40-80 arası** — zorluk sabit kalır
|
|
206
|
+
- Herhangi bir ayarlama öncesinde minimum 3 deneme gerekli (histerezis)
|
|
207
|
+
- Oturumlar 30 dakika hareketsizlik sonra otomatik sona erer
|
|
208
|
+
- Maksimum 10.000 eş zamanlı oturum (en eski otomatik temizlenir)
|
|
209
|
+
|
|
210
|
+
#### Seçenek 1: Constructor'da Session ID
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
const captcha = new K9Guard({
|
|
214
|
+
type: 'math',
|
|
215
|
+
difficulty: 'adaptive',
|
|
216
|
+
sessionId: 'user-123' // herhangi benzersiz string (kullanıcı ID, oturum token'ı, IP vb.)
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const challenge = captcha.generate(); // user-123'ün mevcut zorluğunu kullanır
|
|
220
|
+
const isValid = captcha.validate(challenge, userAnswer); // sonucu otomatik kaydeder
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
#### Seçenek 2: Parametre Olarak Session ID
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
const captcha = new K9Guard({ type: 'math', difficulty: 'adaptive' });
|
|
227
|
+
|
|
228
|
+
// her çağrıda sessionId geçir
|
|
229
|
+
const challenge = captcha.generate('user-123');
|
|
230
|
+
const isValid = captcha.validate(challenge, userAnswer, 'user-123');
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
#### Seçenek 3: Esnek (Her İkisi)
|
|
234
|
+
|
|
235
|
+
Constructor'daki `sessionId` varsayılan olarak kullanılır. Parametre olarak verilen `sessionId` onu override eder.
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
const captcha = new K9Guard({
|
|
239
|
+
type: 'math',
|
|
240
|
+
difficulty: 'adaptive',
|
|
241
|
+
sessionId: 'varsayilan-kullanici'
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
captcha.generate(); // 'varsayilan-kullanici' kullanır
|
|
245
|
+
captcha.generate('diger-kullanici'); // 'diger-kullanici' kullanır (override)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
#### Oturum Yönetimi
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
// oturumun mevcut zorluğunu öğren
|
|
252
|
+
const difficulty = captcha.getSessionDifficulty('user-123'); // 'easy' | 'medium' | 'hard' | null
|
|
253
|
+
|
|
254
|
+
// belirli bir oturumu temizle (zorluk 'medium' olarak sıfırlanır)
|
|
255
|
+
captcha.clearSession('user-123');
|
|
256
|
+
|
|
257
|
+
// tüm oturumları temizle
|
|
258
|
+
captcha.clearAllSessions();
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
#### Tüm CAPTCHA Türleriyle Uyumlu
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
// adaptif zorluk herhangi bir captcha türüyle çalışır
|
|
265
|
+
const captcha = new K9Guard({ type: 'image', difficulty: 'adaptive', sessionId: 'user-1' });
|
|
266
|
+
const captcha = new K9Guard({ type: 'emoji', difficulty: 'adaptive', sessionId: 'user-1' });
|
|
267
|
+
const captcha = new K9Guard({ type: 'reverse', difficulty: 'adaptive', sessionId: 'user-1' });
|
|
268
|
+
// ... ve diğerleri
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
#### Express.js Örneği
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
import express from 'express';
|
|
275
|
+
import K9Guard from 'k9guard';
|
|
276
|
+
|
|
277
|
+
const app = express();
|
|
278
|
+
const captcha = new K9Guard({ type: 'math', difficulty: 'adaptive' });
|
|
279
|
+
|
|
280
|
+
app.get('/captcha', (req, res) => {
|
|
281
|
+
const challenge = captcha.generate(req.sessionID);
|
|
282
|
+
res.json(challenge);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
app.post('/verify', (req, res) => {
|
|
286
|
+
const isValid = captcha.validate(req.body.challenge, req.body.answer, req.sessionID);
|
|
287
|
+
res.json({ valid: isValid });
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
195
291
|
## API Referansı
|
|
196
292
|
|
|
197
293
|
### Yapıcı Metod Seçenekleri
|
|
@@ -203,7 +299,8 @@ const isValid = captcha.validate(challenge, "ankara");
|
|
|
203
299
|
```typescript
|
|
204
300
|
interface K9GuardOptions {
|
|
205
301
|
type: 'math' | 'text' | 'sequence' | 'scramble' | 'reverse' | 'mixed' | 'multi' | 'image' | 'emoji';
|
|
206
|
-
difficulty: 'easy' | 'medium' | 'hard';
|
|
302
|
+
difficulty: 'easy' | 'medium' | 'hard' | 'adaptive';
|
|
303
|
+
sessionId?: string; // opsiyonel, difficulty 'adaptive' iken gerekli
|
|
207
304
|
}
|
|
208
305
|
```
|
|
209
306
|
|
|
@@ -213,6 +310,7 @@ interface K9GuardOptions {
|
|
|
213
310
|
interface K9GuardCustomOptions {
|
|
214
311
|
type: 'custom';
|
|
215
312
|
questions: CustomQuestion[];
|
|
313
|
+
sessionId?: string;
|
|
216
314
|
}
|
|
217
315
|
|
|
218
316
|
interface CustomQuestion {
|
|
@@ -224,10 +322,12 @@ interface CustomQuestion {
|
|
|
224
322
|
|
|
225
323
|
### Metotlar
|
|
226
324
|
|
|
227
|
-
#### `generate(): CaptchaChallenge`
|
|
325
|
+
#### `generate(sessionId?: string): CaptchaChallenge`
|
|
228
326
|
|
|
229
327
|
İstemciye gönderilmesi güvenli bir **public** nesne döndürür — `answer`, `hashedAnswer` ve `salt` çıkarılarak `nonce` ile anahtarlanmış şekilde sunucu tarafında saklanır.
|
|
230
328
|
|
|
329
|
+
`difficulty` `'adaptive'` olduğunda, `sessionId` parametresi kullanıcının mevcut zorluk seviyesini belirlemek için kullanılır. Constructor'da `sessionId` verilmişse varsayılan olarak kullanılır.
|
|
330
|
+
|
|
231
331
|
```typescript
|
|
232
332
|
const challenge = captcha.generate();
|
|
233
333
|
console.log(challenge.question); // kullanıcıya gösterilecek soru
|
|
@@ -239,10 +339,12 @@ console.log(challenge.category); // kategori adı (yalnızca type: 'emoji' içi
|
|
|
239
339
|
// challenge.answer / .hashedAnswer / .salt — MEVCUT DEĞİL; istemciye hiç gönderilmez
|
|
240
340
|
```
|
|
241
341
|
|
|
242
|
-
#### `validate(challenge: CaptchaChallenge, userInput: string): boolean`
|
|
342
|
+
#### `validate(challenge: CaptchaChallenge, userInput: string, sessionId?: string): boolean`
|
|
243
343
|
|
|
244
344
|
Kullanıcı girdisini `challenge.nonce` üzerinden bulunan sunucu taraflı kayıtla karşılaştırır. Doğruysa `true`, yanlışsa `false` döndürür. Public challenge nesnesindeki `hashedAnswer` veya `salt` değiştirme girişimlerinin hiçbir etkisi yoktur.
|
|
245
345
|
|
|
346
|
+
`difficulty` `'adaptive'` olduğunda, doğrulama sonucu otomatik olarak oturuma kaydedilir ve zorluk buna göre ayarlanır.
|
|
347
|
+
|
|
246
348
|
> **⚠️ Tek kullanımlık semantik:** `validate()`, **ilk çağrıda** — cevap doğru ya da yanlış olsun fark etmeksizin — nonce'u tüketir. Her doğrulama denemesinden sonra challenge geçersiz hale gelir. Kullanıcıya yeni bir challenge sunmadan önce mutlaka `generate()` yeniden çağrılmalıdır.
|
|
247
349
|
|
|
248
350
|
```typescript
|
|
@@ -255,6 +357,95 @@ if (!isValid) {
|
|
|
255
357
|
}
|
|
256
358
|
```
|
|
257
359
|
|
|
360
|
+
#### `getSessionDifficulty(sessionId: string): Difficulty | null`
|
|
361
|
+
|
|
362
|
+
Oturumun mevcut adaptif zorluk seviyesini döndürür. Instance adaptif modda değilse `null` döndürür.
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
const difficulty = captcha.getSessionDifficulty('user-123');
|
|
366
|
+
// 'easy' | 'medium' | 'hard' | null
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
#### `clearSession(sessionId: string): boolean`
|
|
370
|
+
|
|
371
|
+
Belirli bir adaptif oturumu temizler. Oturum mevcutsa `true`, değilse `false` döndürür.
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
captcha.clearSession('user-123');
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
#### `clearAllSessions(): void`
|
|
378
|
+
|
|
379
|
+
Tüm adaptif oturumları temizler.
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
captcha.clearAllSessions();
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Dışa Aktarılan Yardımcılar
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
import K9Guard, { AdaptiveTracker, CustomQuestionValidator, CustomQuestionGenerator } from 'k9guard';
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
| Export | Açıklama |
|
|
392
|
+
|--------|----------|
|
|
393
|
+
| `K9Guard` (varsayılan) | Ana CAPTCHA sınıfı |
|
|
394
|
+
| `AdaptiveTracker` | Bağımsız adaptif zorluk takipçisi (özel entegrasyonlar için) |
|
|
395
|
+
| `CustomQuestionValidator` | Özel soru dizilerini doğrula ve sanitize et |
|
|
396
|
+
| `CustomQuestionGenerator` | Özel soru havuzlarından üretim |
|
|
397
|
+
|
|
398
|
+
### Tip Exportları
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
import type {
|
|
402
|
+
K9GuardOptions,
|
|
403
|
+
K9GuardCustomOptions,
|
|
404
|
+
CaptchaChallenge,
|
|
405
|
+
CustomQuestion,
|
|
406
|
+
Difficulty,
|
|
407
|
+
AdaptiveSession,
|
|
408
|
+
AdaptiveAttempt,
|
|
409
|
+
StoredChallenge,
|
|
410
|
+
ImageCaptcha,
|
|
411
|
+
MathCaptcha,
|
|
412
|
+
TextCaptcha,
|
|
413
|
+
SequenceCaptcha,
|
|
414
|
+
ScrambleCaptcha,
|
|
415
|
+
ReverseCaptcha,
|
|
416
|
+
MixedCaptcha,
|
|
417
|
+
CustomCaptcha,
|
|
418
|
+
EmojiCaptcha,
|
|
419
|
+
} from 'k9guard';
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
## Testler
|
|
423
|
+
|
|
424
|
+
K9Guard, `bun:test` kullanarak 228+ test ile birim, entegrasyon, güvenlik, uç durum ve benchmark senaryolarını kapsar.
|
|
425
|
+
|
|
426
|
+
### Testleri Çalıştırma
|
|
427
|
+
|
|
428
|
+
```bash
|
|
429
|
+
# tüm testleri çalıştır
|
|
430
|
+
bun test
|
|
431
|
+
|
|
432
|
+
# izleme modunda çalıştır
|
|
433
|
+
bun run test:watch
|
|
434
|
+
|
|
435
|
+
# kapsam ile çalıştır
|
|
436
|
+
bun run test:coverage
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### Test Kategorileri
|
|
440
|
+
|
|
441
|
+
| Kategori | Kapsam |
|
|
442
|
+
|----------|--------|
|
|
443
|
+
| **Birim** | Her modül bağımsız olarak doğru çıktılar ve uç durumlarla test edilir |
|
|
444
|
+
| **Entegrasyon** | Tüm 10 captcha türü + adaptif mod için tam generate-validate akışı |
|
|
445
|
+
| **Güvenlik** | Timing saldırısı direnci, nonce replay önleme, hash enjeksiyonu, girdi sanitizasyonu, SVG enjeksiyonu |
|
|
446
|
+
| **Uç Durumlar** | Sıfıra bölme, unicode karakterler, eş zamanlı üreteçler, geçersiz girdiler |
|
|
447
|
+
| **Benchmark** | Tüm captcha türleri için performans garantileri (generate < 5ms, validate < 5ms) |
|
|
448
|
+
|
|
258
449
|
## Katkıda Bulunma
|
|
259
450
|
|
|
260
451
|
Katkılarınızı memnuniyetle karşılıyoruz! Nasıl yardımcı olabilirsiniz:
|
|
@@ -262,7 +453,7 @@ Katkılarınızı memnuniyetle karşılıyoruz! Nasıl yardımcı olabilirsiniz:
|
|
|
262
453
|
1. **Depoyu fork edin**
|
|
263
454
|
2. **Özellik dalı oluşturun**: `git checkout -b feature/harika-ozellik`
|
|
264
455
|
3. **Değişiklikleriniz için testler ekleyin**
|
|
265
|
-
4. **Testleri çalıştırın**: `bun
|
|
456
|
+
4. **Testleri çalıştırın**: `bun test`
|
|
266
457
|
5. **Değişikliklerinizi commit edin**: `git commit -m 'feat: harika özellik eklendi'`
|
|
267
458
|
6. **Dalınıza push edin**: `git push origin feature/harika-ozellik`
|
|
268
459
|
7. **Pull Request oluşturun**
|
package/package.json
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "k9guard",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "A secure, lightweight, and flexible CAPTCHA module for TypeScript/JavaScript projects with cryptographic security and multi-language support",
|
|
5
|
-
"main": "index.
|
|
5
|
+
"main": "./dist/index.cjs",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
6
8
|
"type": "module",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
7
21
|
"scripts": {
|
|
8
|
-
"
|
|
22
|
+
"build": "tsup",
|
|
23
|
+
"test": "bun test",
|
|
24
|
+
"test:watch": "bun test --watch",
|
|
25
|
+
"test:coverage": "bun test --coverage",
|
|
26
|
+
"prepublishOnly": "bun run build"
|
|
9
27
|
},
|
|
10
28
|
"keywords": [
|
|
11
29
|
"captcha",
|
|
@@ -42,20 +60,24 @@
|
|
|
42
60
|
"url": "https://github.com/K9Crypt/k9guard/issues"
|
|
43
61
|
},
|
|
44
62
|
"homepage": "https://github.com/K9Crypt/k9guard#readme",
|
|
63
|
+
"files": [
|
|
64
|
+
"dist/",
|
|
65
|
+
"README.md",
|
|
66
|
+
"LICENSE",
|
|
67
|
+
"docs/"
|
|
68
|
+
],
|
|
45
69
|
"engines": {
|
|
46
70
|
"node": ">=16.0.0"
|
|
47
71
|
},
|
|
48
72
|
"devDependencies": {
|
|
49
|
-
"@types/bun": "
|
|
50
|
-
"@types/node": "^
|
|
51
|
-
"@typescript-eslint/eslint-plugin": "^
|
|
52
|
-
"@typescript-eslint/parser": "^
|
|
53
|
-
"eslint": "^
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
|
|
57
|
-
"peerDependencies": {
|
|
58
|
-
"typescript": "^5"
|
|
73
|
+
"@types/bun": "^1.3.14",
|
|
74
|
+
"@types/node": "^25.9.1",
|
|
75
|
+
"@typescript-eslint/eslint-plugin": "^8.59.4",
|
|
76
|
+
"@typescript-eslint/parser": "^8.59.4",
|
|
77
|
+
"eslint": "^10.4.0",
|
|
78
|
+
"tsup": "^8.5.1",
|
|
79
|
+
"tsx": "^4.22.3",
|
|
80
|
+
"typescript": "^6.0.3"
|
|
59
81
|
},
|
|
60
82
|
"publishConfig": {
|
|
61
83
|
"access": "public",
|
package/index.ts
DELETED
package/src/K9Guard.ts
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import type { K9GuardOptions, K9GuardCustomOptions, CaptchaChallenge, CustomQuestion } from './types';
|
|
2
|
-
import { CaptchaGenerator } from './core/captchaGenerator';
|
|
3
|
-
import { CaptchaValidator } from './core/captchaValidator';
|
|
4
|
-
import { CustomQuestionValidator } from './validators/customQuestionValidator';
|
|
5
|
-
|
|
6
|
-
export class K9Guard {
|
|
7
|
-
private options: K9GuardOptions | K9GuardCustomOptions;
|
|
8
|
-
private generator: CaptchaGenerator;
|
|
9
|
-
|
|
10
|
-
constructor(options: K9GuardOptions | K9GuardCustomOptions | { type: 'custom'; questions: CustomQuestion[] }) {
|
|
11
|
-
const processedOptions = this.processOptions(options);
|
|
12
|
-
this.generator = new CaptchaGenerator(processedOptions);
|
|
13
|
-
this.options = processedOptions;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
private processOptions(options: unknown): K9GuardOptions | K9GuardCustomOptions {
|
|
17
|
-
if (typeof options !== 'object' || options === null) {
|
|
18
|
-
throw new Error('Options must be an object');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const opt = options as Record<string, unknown>;
|
|
22
|
-
|
|
23
|
-
if (opt.type === 'custom') {
|
|
24
|
-
if (!Array.isArray(opt.questions)) {
|
|
25
|
-
throw new Error('Custom type requires questions array');
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const validation = CustomQuestionValidator.validate(opt.questions);
|
|
29
|
-
if (!validation.valid) {
|
|
30
|
-
throw new Error(`Invalid custom questions: ${validation.error}`);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
type: 'custom',
|
|
35
|
-
questions: CustomQuestionValidator.sanitize(opt.questions as CustomQuestion[])
|
|
36
|
-
} as K9GuardCustomOptions;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const validTypes = ['math', 'text', 'sequence', 'scramble', 'reverse', 'mixed', 'multi', 'image', 'emoji'] as const;
|
|
40
|
-
if (!validTypes.includes(opt.type as any)) {
|
|
41
|
-
throw new Error(`Invalid type. Must be one of: ${validTypes.join(', ')}`);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const validDifficulties = ['easy', 'medium', 'hard'] as const;
|
|
45
|
-
if (!validDifficulties.includes(opt.difficulty as any)) {
|
|
46
|
-
throw new Error(`Invalid difficulty. Must be one of: ${validDifficulties.join(', ')}`);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
type: opt.type,
|
|
51
|
-
difficulty: opt.difficulty
|
|
52
|
-
} as K9GuardOptions;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
generate(): CaptchaChallenge {
|
|
56
|
-
return this.generator.generate();
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
validate(challenge: CaptchaChallenge, userInput: string): boolean {
|
|
60
|
-
if (!this.isValidChallenge(challenge)) {
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (typeof userInput !== 'string') {
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// consume() atomically removes the nonce from the store — single-use semantics.
|
|
69
|
-
// hashedAnswer and salt come from the server-side store, never from the client,
|
|
70
|
-
// which prevents hash-injection and replay attacks.
|
|
71
|
-
const stored = this.generator.consume(challenge.nonce);
|
|
72
|
-
if (!stored) {
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (Date.now() > stored.expiry) {
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return CaptchaValidator.validate(stored, userInput);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
private isValidChallenge(challenge: unknown): boolean {
|
|
84
|
-
if (typeof challenge !== 'object' || challenge === null) {
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const c = challenge as Record<string, unknown>;
|
|
89
|
-
|
|
90
|
-
return (
|
|
91
|
-
typeof c.type === 'string' &&
|
|
92
|
-
typeof c.question === 'string' &&
|
|
93
|
-
typeof c.nonce === 'string' && c.nonce.length > 0 &&
|
|
94
|
-
typeof c.expiry === 'number'
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
}
|