k9guard 1.0.1 → 1.0.3
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/LICENSE +21 -0
- package/README.md +86 -49
- package/docs/tr/README.md +88 -51
- package/package.json +7 -7
- package/src/K9Guard.ts +26 -12
- package/src/core/captchaGenerator.ts +207 -68
- package/src/core/captchaValidator.ts +96 -15
- package/src/types/index.ts +34 -22
- package/src/utils/crypto.ts +234 -258
- package/src/utils/emojiGenerator.ts +88 -0
- package/src/utils/imageGenerator.ts +154 -0
- package/src/utils/random.ts +2 -2
- package/src/utils/reverseGenerator.ts +5 -1
- package/src/utils/scrambleGenerator.ts +25 -13
- package/src/utils/sequenceGenerator.ts +15 -14
- package/src/locale/en.json +0 -46
- package/src/locale/tr.json +0 -46
- package/src/utils/logicGenerator.ts +0 -18
- package/src/utils/riddleBank.ts +0 -18
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 K9Crypt
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -4,15 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
# K9Guard
|
|
6
6
|
|
|
7
|
-
A secure, lightweight, and flexible CAPTCHA module for TypeScript/JavaScript projects with cryptographic security
|
|
7
|
+
A secure, lightweight, and flexible CAPTCHA module for TypeScript/JavaScript projects with cryptographic security.
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
11
|
- **Cryptographically Secure**: NIST SP 800-90A compliant random generation
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
15
|
-
- **
|
|
12
|
+
- **10 CAPTCHA Types**: Math, text, sequence, scramble, reverse, mixed, multi-step, image, emoji, and custom challenges
|
|
13
|
+
- **Security First**: SHA-256 salted hashing, server-side challenge store, nonce-based session management, and 5-minute expiry
|
|
14
|
+
- **Single-Use Challenges**: Every nonce is consumed on the first `validate()` call — success or failure — preventing replay and brute-force attacks
|
|
15
|
+
- **Strict Configuration**: Invalid `type` or `difficulty` values throw immediately; no silent fallbacks
|
|
16
|
+
- **Input Validation**: Length limits, strict numeric parsing, type checking, and sanitization to prevent injection attacks
|
|
16
17
|
- **Custom Questions**: Support for your own questions with validation and sanitization
|
|
17
18
|
- **Zero Dependencies**: Lightweight with no external dependencies
|
|
18
19
|
- **Well Tested**: Comprehensive test coverage including edge cases and security scenarios
|
|
@@ -32,8 +33,7 @@ import K9Guard from "k9guard";
|
|
|
32
33
|
|
|
33
34
|
const captcha = new K9Guard({
|
|
34
35
|
type: 'math',
|
|
35
|
-
difficulty: 'medium'
|
|
36
|
-
locale: 'en'
|
|
36
|
+
difficulty: 'medium'
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
// generate a challenge
|
|
@@ -69,15 +69,6 @@ const challenge = captcha.generate();
|
|
|
69
69
|
// Answer: "aB2xY9"
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
### Riddle CAPTCHA
|
|
73
|
-
|
|
74
|
-
```typescript
|
|
75
|
-
const captcha = new K9Guard({ type: 'riddle', difficulty: 'easy', locale: 'en' });
|
|
76
|
-
const challenge = captcha.generate();
|
|
77
|
-
// Output: "What has keys but can't open locks?"
|
|
78
|
-
// Answer: "piano"
|
|
79
|
-
```
|
|
80
|
-
|
|
81
72
|
### Sequence CAPTCHA
|
|
82
73
|
|
|
83
74
|
```typescript
|
|
@@ -96,24 +87,73 @@ const challenge = captcha.generate();
|
|
|
96
87
|
// Answer: "cat"
|
|
97
88
|
```
|
|
98
89
|
|
|
99
|
-
###
|
|
90
|
+
### Reverse CAPTCHA
|
|
100
91
|
|
|
101
92
|
```typescript
|
|
102
|
-
const captcha = new K9Guard({ type: '
|
|
93
|
+
const captcha = new K9Guard({ type: 'reverse', difficulty: 'easy' });
|
|
103
94
|
const challenge = captcha.generate();
|
|
104
|
-
// Output: "
|
|
105
|
-
// Answer: "
|
|
95
|
+
// Output: "god"
|
|
96
|
+
// Answer: "dog"
|
|
106
97
|
```
|
|
107
98
|
|
|
108
|
-
###
|
|
99
|
+
### Image CAPTCHA
|
|
109
100
|
|
|
110
101
|
```typescript
|
|
111
|
-
const captcha = new K9Guard({ type: '
|
|
102
|
+
const captcha = new K9Guard({ type: 'image', difficulty: 'medium' });
|
|
112
103
|
const challenge = captcha.generate();
|
|
113
|
-
|
|
114
|
-
//
|
|
104
|
+
|
|
105
|
+
// challenge.image — base64 SVG data URI, render it directly in an <img> tag
|
|
106
|
+
// challenge.question — "Type the characters shown in the image"
|
|
107
|
+
console.log(challenge.image); // "data:image/svg+xml;base64,..."
|
|
108
|
+
|
|
109
|
+
// validate user input (case-insensitive)
|
|
110
|
+
const isValid = captcha.validate(challenge, "aB3z");
|
|
111
|
+
if (isValid) {
|
|
112
|
+
console.log("Access granted!");
|
|
113
|
+
} else {
|
|
114
|
+
console.log("Wrong answer!");
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The image is a distorted SVG with:
|
|
119
|
+
- **Rotated & offset characters** per-glyph, randomized color and size
|
|
120
|
+
- **Sinusoidal wave overlays** proportional to difficulty
|
|
121
|
+
- **Noise lines and dots** that break simple segmentation attacks
|
|
122
|
+
- **Case-insensitive validation** — user may type upper or lowercase
|
|
123
|
+
- **No external dependencies** — pure SVG generated server-side
|
|
124
|
+
|
|
125
|
+
### Emoji CAPTCHA
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
const captcha = new K9Guard({ type: 'emoji', difficulty: 'medium' });
|
|
129
|
+
const challenge = captcha.generate();
|
|
130
|
+
|
|
131
|
+
// challenge.emojis — array of emojis to display (6 for medium)
|
|
132
|
+
// challenge.category — the target category name (e.g. "animals")
|
|
133
|
+
// challenge.question — "Select all animals from the list (6 emojis, 3 correct)"
|
|
134
|
+
console.log(challenge.emojis); // ["🐶", "🍎", "🚗", "🐱", "🌸", "🏀"]
|
|
135
|
+
console.log(challenge.category); // "animals"
|
|
136
|
+
|
|
137
|
+
// user submits sorted comma-separated zero-based indices of the correct emojis
|
|
138
|
+
// e.g. if emojis[0] and emojis[3] are animals: "0,3"
|
|
139
|
+
const isValid = captcha.validate(challenge, "0,3");
|
|
140
|
+
if (isValid) {
|
|
141
|
+
console.log("Access granted!");
|
|
142
|
+
} else {
|
|
143
|
+
console.log("Wrong answer!");
|
|
144
|
+
}
|
|
115
145
|
```
|
|
116
146
|
|
|
147
|
+
Difficulty controls the number of emojis shown and correct answers required:
|
|
148
|
+
|
|
149
|
+
| Difficulty | Total emojis | Correct to select |
|
|
150
|
+
|------------|-------------|-------------------|
|
|
151
|
+
| easy | 4 | 2 |
|
|
152
|
+
| medium | 6 | 3 |
|
|
153
|
+
| hard | 8 | 4 |
|
|
154
|
+
|
|
155
|
+
There are 5 categories (animals, food, vehicles, nature, sports) with 20 emojis each. Distractors are drawn from all other categories. Answer format: sorted comma-separated zero-based indices, e.g. `"0,2,4"`.
|
|
156
|
+
|
|
117
157
|
### Mixed CAPTCHA
|
|
118
158
|
|
|
119
159
|
```typescript
|
|
@@ -129,9 +169,9 @@ const captcha = new K9Guard({ type: 'multi', difficulty: 'easy' });
|
|
|
129
169
|
const challenge = captcha.generate();
|
|
130
170
|
|
|
131
171
|
if (challenge.steps) {
|
|
132
|
-
// user must solve both steps
|
|
133
|
-
|
|
134
|
-
const userInput = JSON.stringify(
|
|
172
|
+
// user must solve both steps; steps expose only question/nonce/expiry — not the answer
|
|
173
|
+
// answers are submitted as a JSON array of strings
|
|
174
|
+
const userInput = JSON.stringify(["22", "typescript"]);
|
|
135
175
|
const isValid = captcha.validate(challenge, userInput);
|
|
136
176
|
}
|
|
137
177
|
```
|
|
@@ -156,13 +196,14 @@ const isValid = captcha.validate(challenge, "paris");
|
|
|
156
196
|
|
|
157
197
|
### Constructor Options
|
|
158
198
|
|
|
199
|
+
Both `type` and `difficulty` are **required** and strictly validated. Passing an invalid value throws an error immediately.
|
|
200
|
+
|
|
159
201
|
#### Standard CAPTCHA Options
|
|
160
202
|
|
|
161
203
|
```typescript
|
|
162
204
|
interface K9GuardOptions {
|
|
163
|
-
type: 'math' | 'text' | '
|
|
205
|
+
type: 'math' | 'text' | 'sequence' | 'scramble' | 'reverse' | 'mixed' | 'multi' | 'image' | 'emoji';
|
|
164
206
|
difficulty: 'easy' | 'medium' | 'hard';
|
|
165
|
-
locale?: 'en' | 'tr'; // default: 'en'
|
|
166
207
|
}
|
|
167
208
|
```
|
|
168
209
|
|
|
@@ -185,39 +226,35 @@ interface CustomQuestion {
|
|
|
185
226
|
|
|
186
227
|
#### `generate(): CaptchaChallenge`
|
|
187
228
|
|
|
188
|
-
Generates a new CAPTCHA challenge
|
|
229
|
+
Generates a new CAPTCHA challenge. Returns a **public** object safe to send to the client — `answer`, `hashedAnswer` and `salt` are stripped and stored server-side, keyed by `nonce`.
|
|
189
230
|
|
|
190
231
|
```typescript
|
|
191
232
|
const challenge = captcha.generate();
|
|
192
|
-
console.log(challenge.question);
|
|
193
|
-
console.log(challenge.nonce);
|
|
194
|
-
console.log(challenge.expiry);
|
|
233
|
+
console.log(challenge.question); // the question to show the user
|
|
234
|
+
console.log(challenge.nonce); // unique session identifier (pass back on validate)
|
|
235
|
+
console.log(challenge.expiry); // Unix ms timestamp when challenge expires
|
|
236
|
+
console.log(challenge.image); // base64 SVG data URI (only for type: 'image')
|
|
237
|
+
console.log(challenge.emojis); // emoji array (only for type: 'emoji')
|
|
238
|
+
console.log(challenge.category); // category name (only for type: 'emoji')
|
|
239
|
+
// challenge.answer / .hashedAnswer / .salt — NOT present; never sent to client
|
|
195
240
|
```
|
|
196
241
|
|
|
197
242
|
#### `validate(challenge: CaptchaChallenge, userInput: string): boolean`
|
|
198
243
|
|
|
199
|
-
Validates user input against the challenge. Returns `true` if correct, `false` otherwise.
|
|
244
|
+
Validates user input against the stored server-side record (looked up by `challenge.nonce`). Returns `true` if correct, `false` otherwise. Tampered `hashedAnswer` or `salt` on the public challenge object have no effect.
|
|
245
|
+
|
|
246
|
+
> **⚠️ Single-use semantics:** `validate()` consumes the nonce on the **first call**, regardless of whether the answer is correct or not. After any validation attempt, the challenge is invalidated. Always call `generate()` again before presenting a new challenge to the user.
|
|
200
247
|
|
|
201
248
|
```typescript
|
|
202
249
|
const isValid = captcha.validate(challenge, userAnswer);
|
|
203
|
-
```
|
|
204
250
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
bun run src/test.ts
|
|
251
|
+
// After validate(), the challenge is consumed.
|
|
252
|
+
// For a retry, generate a fresh challenge:
|
|
253
|
+
if (!isValid) {
|
|
254
|
+
const newChallenge = captcha.generate();
|
|
255
|
+
}
|
|
211
256
|
```
|
|
212
257
|
|
|
213
|
-
Tests include:
|
|
214
|
-
- All CAPTCHA types with correct/incorrect/edge case inputs
|
|
215
|
-
- Custom question validation
|
|
216
|
-
- Locale switching
|
|
217
|
-
- Multi-step challenges
|
|
218
|
-
- Input sanitization
|
|
219
|
-
- Security validations
|
|
220
|
-
|
|
221
258
|
## Contributing
|
|
222
259
|
|
|
223
260
|
We welcome contributions! Here's how you can help:
|
package/docs/tr/README.md
CHANGED
|
@@ -4,15 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
# K9Guard
|
|
6
6
|
|
|
7
|
-
TypeScript/JavaScript projeleri için kriptografik güvenlik
|
|
7
|
+
TypeScript/JavaScript projeleri için kriptografik güvenlik sunan güvenli, hafif ve esnek bir CAPTCHA modülü.
|
|
8
8
|
|
|
9
9
|
## Özellikler
|
|
10
10
|
|
|
11
11
|
- **Kriptografik Güvenlik**: NIST SP 800-90A standardına uyumluluk sağlanmıştır
|
|
12
|
-
-
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
15
|
-
- **
|
|
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
|
+
- **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
|
+
- **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
|
+
- **Katı Yapılandırma**: Geçersiz `type` veya `difficulty` değerleri anında hata fırlatır; sessiz fallback yoktur
|
|
16
|
+
- **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
|
|
16
17
|
- **Özel Sorular**: Doğrulama ve sanitizasyon ile kendi sorularınızı tanımlama desteği
|
|
17
18
|
- **Sıfır Bağımlılık**: Harici bağımlılık gerektirmeyen hafif yapı
|
|
18
19
|
- **Kapsamlı Test Edilmiş**: Uç durumlar ve güvenlik senaryoları dahil olmak üzere geniş test kapsama alanı
|
|
@@ -32,8 +33,7 @@ import K9Guard from "k9guard";
|
|
|
32
33
|
|
|
33
34
|
const captcha = new K9Guard({
|
|
34
35
|
type: 'math',
|
|
35
|
-
difficulty: 'medium'
|
|
36
|
-
locale: 'tr'
|
|
36
|
+
difficulty: 'medium'
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
// doğrulama sorusu oluştur
|
|
@@ -69,15 +69,6 @@ const challenge = captcha.generate();
|
|
|
69
69
|
// Cevap: "aB2xY9"
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
### Bulmaca CAPTCHA
|
|
73
|
-
|
|
74
|
-
```typescript
|
|
75
|
-
const captcha = new K9Guard({ type: 'riddle', difficulty: 'easy', locale: 'tr' });
|
|
76
|
-
const challenge = captcha.generate();
|
|
77
|
-
// Çıktı: "Tuşları vardır ama kilit açamaz. Nedir?"
|
|
78
|
-
// Cevap: "piyano"
|
|
79
|
-
```
|
|
80
|
-
|
|
81
72
|
### Dizi CAPTCHA
|
|
82
73
|
|
|
83
74
|
```typescript
|
|
@@ -92,28 +83,77 @@ const challenge = captcha.generate();
|
|
|
92
83
|
```typescript
|
|
93
84
|
const captcha = new K9Guard({ type: 'scramble', difficulty: 'easy' });
|
|
94
85
|
const challenge = captcha.generate();
|
|
95
|
-
// Çıktı: "
|
|
96
|
-
// Cevap: "
|
|
86
|
+
// Çıktı: "tac"
|
|
87
|
+
// Cevap: "cat"
|
|
97
88
|
```
|
|
98
89
|
|
|
99
|
-
###
|
|
90
|
+
### Ters Çevirme CAPTCHA
|
|
100
91
|
|
|
101
92
|
```typescript
|
|
102
|
-
const captcha = new K9Guard({ type: '
|
|
93
|
+
const captcha = new K9Guard({ type: 'reverse', difficulty: 'easy' });
|
|
103
94
|
const challenge = captcha.generate();
|
|
104
|
-
// Çıktı: "
|
|
105
|
-
// Cevap: "
|
|
95
|
+
// Çıktı: "god"
|
|
96
|
+
// Cevap: "dog"
|
|
106
97
|
```
|
|
107
98
|
|
|
108
|
-
###
|
|
99
|
+
### Görsel CAPTCHA
|
|
109
100
|
|
|
110
101
|
```typescript
|
|
111
|
-
const captcha = new K9Guard({ type: '
|
|
102
|
+
const captcha = new K9Guard({ type: 'image', difficulty: 'medium' });
|
|
103
|
+
const challenge = captcha.generate();
|
|
104
|
+
|
|
105
|
+
// challenge.image — doğrudan <img> etiketinde kullanılabilecek base64 SVG data URI
|
|
106
|
+
// challenge.question — "Type the characters shown in the image"
|
|
107
|
+
console.log(challenge.image); // "data:image/svg+xml;base64,..."
|
|
108
|
+
|
|
109
|
+
// kullanıcı yanıtını doğrula (büyük/küçük harf duyarsız)
|
|
110
|
+
const isValid = captcha.validate(challenge, "aB3z");
|
|
111
|
+
if (isValid) {
|
|
112
|
+
console.log("Erişim izni verildi!");
|
|
113
|
+
} else {
|
|
114
|
+
console.log("Yanlış cevap!");
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Görsel CAPTCHA'nın güvenlik özellikleri:
|
|
119
|
+
- **Karakter başına rotasyon ve offset** — rastgele renk ve boyutla OCR direnci
|
|
120
|
+
- **Sinüzoidal dalga katmanları** — zorluk seviyesine orantılı üst üste bindirilir
|
|
121
|
+
- **Gürültü çizgileri ve noktaları** — basit segmentasyon saldırılarını engeller
|
|
122
|
+
- **Büyük/küçük harf duyarsız doğrulama** — kullanıcı hem büyük hem küçük harf girebilir
|
|
123
|
+
- **Sıfır dış bağımlılık** — tamamen sunucu tarafında saf SVG ile üretilir
|
|
124
|
+
|
|
125
|
+
### Emoji CAPTCHA
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
const captcha = new K9Guard({ type: 'emoji', difficulty: 'medium' });
|
|
112
129
|
const challenge = captcha.generate();
|
|
113
|
-
|
|
114
|
-
//
|
|
130
|
+
|
|
131
|
+
// challenge.emojis — gösterilecek emoji dizisi (medium için 6 adet)
|
|
132
|
+
// challenge.category — hedef kategori adı (örn. "animals")
|
|
133
|
+
// challenge.question — "Select all animals from the list (6 emojis, 3 correct)"
|
|
134
|
+
console.log(challenge.emojis); // ["🐶", "🍎", "🚗", "🐱", "🌸", "🏀"]
|
|
135
|
+
console.log(challenge.category); // "animals"
|
|
136
|
+
|
|
137
|
+
// kullanıcı, doğru emojilerin sıfır tabanlı indekslerini virgülle ayırarak gönderir
|
|
138
|
+
// örn. emojis[0] ve emojis[3] hayvan ise: "0,3"
|
|
139
|
+
const isValid = captcha.validate(challenge, "0,3");
|
|
140
|
+
if (isValid) {
|
|
141
|
+
console.log("Erişim izni verildi!");
|
|
142
|
+
} else {
|
|
143
|
+
console.log("Yanlış cevap!");
|
|
144
|
+
}
|
|
115
145
|
```
|
|
116
146
|
|
|
147
|
+
Zorluk seviyesi gösterilen emoji sayısını ve doğru seçilmesi gereken emoji sayısını belirler:
|
|
148
|
+
|
|
149
|
+
| Zorluk | Toplam emoji | Seçilmesi gereken |
|
|
150
|
+
|---------|-------------|-------------------|
|
|
151
|
+
| easy | 4 | 2 |
|
|
152
|
+
| medium | 6 | 3 |
|
|
153
|
+
| hard | 8 | 4 |
|
|
154
|
+
|
|
155
|
+
5 kategori mevcuttur (animals, food, vehicles, nature, sports), her birinde 20 emoji bulunur. Yanıltıcı emojiler diğer kategorilerden seçilir. Cevap formatı: sıralanmış, virgülle ayrılmış sıfır tabanlı indeksler; örn. `"0,2,4"`.
|
|
156
|
+
|
|
117
157
|
### Karma CAPTCHA
|
|
118
158
|
|
|
119
159
|
```typescript
|
|
@@ -129,9 +169,9 @@ const captcha = new K9Guard({ type: 'multi', difficulty: 'easy' });
|
|
|
129
169
|
const challenge = captcha.generate();
|
|
130
170
|
|
|
131
171
|
if (challenge.steps) {
|
|
132
|
-
// kullanıcı her iki adımı da çözmelidir
|
|
133
|
-
|
|
134
|
-
const userInput = JSON.stringify(
|
|
172
|
+
// kullanıcı her iki adımı da çözmelidir; steps yalnızca question/nonce/expiry içerir
|
|
173
|
+
// cevaplar JSON dizisi olarak gönderilir
|
|
174
|
+
const userInput = JSON.stringify(["22", "typescript"]);
|
|
135
175
|
const isValid = captcha.validate(challenge, userInput);
|
|
136
176
|
}
|
|
137
177
|
```
|
|
@@ -156,13 +196,14 @@ const isValid = captcha.validate(challenge, "ankara");
|
|
|
156
196
|
|
|
157
197
|
### Yapıcı Metod Seçenekleri
|
|
158
198
|
|
|
199
|
+
`type` ve `difficulty` alanları **zorunludur** ve katı şekilde doğrulanır. Geçersiz bir değer iletildiğinde constructor anında hata fırlatır.
|
|
200
|
+
|
|
159
201
|
#### Standart CAPTCHA Seçenekleri
|
|
160
202
|
|
|
161
203
|
```typescript
|
|
162
204
|
interface K9GuardOptions {
|
|
163
|
-
type: 'math' | 'text' | '
|
|
205
|
+
type: 'math' | 'text' | 'sequence' | 'scramble' | 'reverse' | 'mixed' | 'multi' | 'image' | 'emoji';
|
|
164
206
|
difficulty: 'easy' | 'medium' | 'hard';
|
|
165
|
-
locale?: 'en' | 'tr'; // varsayılan: 'en'
|
|
166
207
|
}
|
|
167
208
|
```
|
|
168
209
|
|
|
@@ -185,39 +226,35 @@ interface CustomQuestion {
|
|
|
185
226
|
|
|
186
227
|
#### `generate(): CaptchaChallenge`
|
|
187
228
|
|
|
188
|
-
|
|
229
|
+
İ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.
|
|
189
230
|
|
|
190
231
|
```typescript
|
|
191
232
|
const challenge = captcha.generate();
|
|
192
|
-
console.log(challenge.question);
|
|
193
|
-
console.log(challenge.nonce);
|
|
194
|
-
console.log(challenge.expiry);
|
|
233
|
+
console.log(challenge.question); // kullanıcıya gösterilecek soru
|
|
234
|
+
console.log(challenge.nonce); // benzersiz oturum tanımlayıcısı (validate'e geri gönderilir)
|
|
235
|
+
console.log(challenge.expiry); // Unix ms cinsinden geçerlilik bitiş zamanı
|
|
236
|
+
console.log(challenge.image); // base64 SVG data URI (yalnızca type: 'image' için)
|
|
237
|
+
console.log(challenge.emojis); // emoji dizisi (yalnızca type: 'emoji' için)
|
|
238
|
+
console.log(challenge.category); // kategori adı (yalnızca type: 'emoji' için)
|
|
239
|
+
// challenge.answer / .hashedAnswer / .salt — MEVCUT DEĞİL; istemciye hiç gönderilmez
|
|
195
240
|
```
|
|
196
241
|
|
|
197
242
|
#### `validate(challenge: CaptchaChallenge, userInput: string): boolean`
|
|
198
243
|
|
|
199
|
-
Kullanıcı girdisini
|
|
244
|
+
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
|
+
|
|
246
|
+
> **⚠️ 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.
|
|
200
247
|
|
|
201
248
|
```typescript
|
|
202
249
|
const isValid = captcha.validate(challenge, userAnswer);
|
|
203
|
-
```
|
|
204
250
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
bun run src/test.ts
|
|
251
|
+
// validate() çağrısından sonra challenge tüketilir.
|
|
252
|
+
// Yeniden deneme için yeni bir challenge üretilmeli:
|
|
253
|
+
if (!isValid) {
|
|
254
|
+
const newChallenge = captcha.generate();
|
|
255
|
+
}
|
|
211
256
|
```
|
|
212
257
|
|
|
213
|
-
Testler şunları içerir:
|
|
214
|
-
- Tüm CAPTCHA türleri için doğru/yanlış/uç durum girdileri
|
|
215
|
-
- Özel soru doğrulama senaryoları
|
|
216
|
-
- Dil değiştirme işlemleri
|
|
217
|
-
- Çok adımlı doğrulamalar
|
|
218
|
-
- Girdi sanitizasyonu
|
|
219
|
-
- Güvenlik doğrulamaları
|
|
220
|
-
|
|
221
258
|
## Katkıda Bulunma
|
|
222
259
|
|
|
223
260
|
Katkılarınızı memnuniyetle karşılıyoruz! Nasıl yardımcı olabilirsiniz:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "k9guard",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "A secure, lightweight, and flexible CAPTCHA module for TypeScript/JavaScript projects with cryptographic security and multi-language support",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -47,12 +47,12 @@
|
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/bun": "latest",
|
|
50
|
-
"@types/node": "^20.
|
|
51
|
-
"@typescript-eslint/eslint-plugin": "^6.
|
|
52
|
-
"@typescript-eslint/parser": "^6.
|
|
53
|
-
"eslint": "^8.
|
|
54
|
-
"tsx": "^4.
|
|
55
|
-
"typescript": "^5.
|
|
50
|
+
"@types/node": "^20.19.33",
|
|
51
|
+
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
|
52
|
+
"@typescript-eslint/parser": "^6.21.0",
|
|
53
|
+
"eslint": "^8.57.1",
|
|
54
|
+
"tsx": "^4.21.0",
|
|
55
|
+
"typescript": "^5.9.3"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
58
|
"typescript": "^5"
|
package/src/K9Guard.ts
CHANGED
|
@@ -7,7 +7,7 @@ export class K9Guard {
|
|
|
7
7
|
private options: K9GuardOptions | K9GuardCustomOptions;
|
|
8
8
|
private generator: CaptchaGenerator;
|
|
9
9
|
|
|
10
|
-
constructor(options: K9GuardOptions | K9GuardCustomOptions | { type: 'custom'; questions: CustomQuestion[] }
|
|
10
|
+
constructor(options: K9GuardOptions | K9GuardCustomOptions | { type: 'custom'; questions: CustomQuestion[] }) {
|
|
11
11
|
const processedOptions = this.processOptions(options);
|
|
12
12
|
this.generator = new CaptchaGenerator(processedOptions);
|
|
13
13
|
this.options = processedOptions;
|
|
@@ -15,7 +15,7 @@ export class K9Guard {
|
|
|
15
15
|
|
|
16
16
|
private processOptions(options: unknown): K9GuardOptions | K9GuardCustomOptions {
|
|
17
17
|
if (typeof options !== 'object' || options === null) {
|
|
18
|
-
|
|
18
|
+
throw new Error('Options must be an object');
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
const opt = options as Record<string, unknown>;
|
|
@@ -36,10 +36,19 @@ export class K9Guard {
|
|
|
36
36
|
} as K9GuardCustomOptions;
|
|
37
37
|
}
|
|
38
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
|
+
|
|
39
49
|
return {
|
|
40
|
-
type: opt.type
|
|
41
|
-
difficulty: opt.difficulty
|
|
42
|
-
locale: opt.locale || 'en'
|
|
50
|
+
type: opt.type,
|
|
51
|
+
difficulty: opt.difficulty
|
|
43
52
|
} as K9GuardOptions;
|
|
44
53
|
}
|
|
45
54
|
|
|
@@ -56,12 +65,19 @@ export class K9Guard {
|
|
|
56
65
|
return false;
|
|
57
66
|
}
|
|
58
67
|
|
|
59
|
-
|
|
60
|
-
|
|
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) {
|
|
61
77
|
return false;
|
|
62
78
|
}
|
|
63
79
|
|
|
64
|
-
return CaptchaValidator.validate(
|
|
80
|
+
return CaptchaValidator.validate(stored, userInput);
|
|
65
81
|
}
|
|
66
82
|
|
|
67
83
|
private isValidChallenge(challenge: unknown): boolean {
|
|
@@ -74,10 +90,8 @@ export class K9Guard {
|
|
|
74
90
|
return (
|
|
75
91
|
typeof c.type === 'string' &&
|
|
76
92
|
typeof c.question === 'string' &&
|
|
77
|
-
typeof c.nonce === 'string' &&
|
|
78
|
-
typeof c.expiry === 'number'
|
|
79
|
-
typeof c.hashedAnswer === 'string' &&
|
|
80
|
-
typeof c.salt === 'string'
|
|
93
|
+
typeof c.nonce === 'string' && c.nonce.length > 0 &&
|
|
94
|
+
typeof c.expiry === 'number'
|
|
81
95
|
);
|
|
82
96
|
}
|
|
83
97
|
}
|