k9guard 1.0.1 → 1.0.2

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 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,14 +4,13 @@
4
4
 
5
5
  # K9Guard
6
6
 
7
- A secure, lightweight, and flexible CAPTCHA module for TypeScript/JavaScript projects with cryptographic security and multi-language support.
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
- - **Multi-Language Support**: English and Turkish locales for riddles and logic questions
13
- - **9 CAPTCHA Types**: Math, text, riddle, sequence, scramble, logic, reverse, mixed, and multi-step challenges
14
- - **Security First**: SHA-256 salted hashing, nonce-based session management, and 5-minute expiry
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
15
14
  - **Input Validation**: Length limits, type checking, and sanitization to prevent injection attacks
16
15
  - **Custom Questions**: Support for your own questions with validation and sanitization
17
16
  - **Zero Dependencies**: Lightweight with no external dependencies
@@ -32,8 +31,7 @@ import K9Guard from "k9guard";
32
31
 
33
32
  const captcha = new K9Guard({
34
33
  type: 'math',
35
- difficulty: 'medium',
36
- locale: 'en'
34
+ difficulty: 'medium'
37
35
  });
38
36
 
39
37
  // generate a challenge
@@ -69,15 +67,6 @@ const challenge = captcha.generate();
69
67
  // Answer: "aB2xY9"
70
68
  ```
71
69
 
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
70
  ### Sequence CAPTCHA
82
71
 
83
72
  ```typescript
@@ -96,24 +85,73 @@ const challenge = captcha.generate();
96
85
  // Answer: "cat"
97
86
  ```
98
87
 
99
- ### Logic CAPTCHA
88
+ ### Reverse CAPTCHA
100
89
 
101
90
  ```typescript
102
- const captcha = new K9Guard({ type: 'logic', difficulty: 'easy', locale: 'en' });
91
+ const captcha = new K9Guard({ type: 'reverse', difficulty: 'easy' });
103
92
  const challenge = captcha.generate();
104
- // Output: "Water is dry. True or False?"
105
- // Answer: "false"
93
+ // Output: "god"
94
+ // Answer: "dog"
106
95
  ```
107
96
 
108
- ### Reverse CAPTCHA
97
+ ### Image CAPTCHA
109
98
 
110
99
  ```typescript
111
- const captcha = new K9Guard({ type: 'reverse', difficulty: 'easy' });
100
+ const captcha = new K9Guard({ type: 'image', difficulty: 'medium' });
112
101
  const challenge = captcha.generate();
113
- // Output: "god"
114
- // Answer: "dog"
102
+
103
+ // challenge.image — base64 SVG data URI, render it directly in an <img> tag
104
+ // challenge.question — "Type the characters shown in the image"
105
+ console.log(challenge.image); // "data:image/svg+xml;base64,..."
106
+
107
+ // validate user input (case-insensitive)
108
+ const isValid = captcha.validate(challenge, "aB3z");
109
+ if (isValid) {
110
+ console.log("Access granted!");
111
+ } else {
112
+ console.log("Wrong answer!");
113
+ }
114
+ ```
115
+
116
+ The image is a distorted SVG with:
117
+ - **Rotated & offset characters** per-glyph, randomized color and size
118
+ - **Sinusoidal wave overlays** proportional to difficulty
119
+ - **Noise lines and dots** that break simple segmentation attacks
120
+ - **Case-insensitive validation** — user may type upper or lowercase
121
+ - **No external dependencies** — pure SVG generated server-side
122
+
123
+ ### Emoji CAPTCHA
124
+
125
+ ```typescript
126
+ const captcha = new K9Guard({ type: 'emoji', difficulty: 'medium' });
127
+ const challenge = captcha.generate();
128
+
129
+ // challenge.emojis — array of emojis to display (6 for medium)
130
+ // challenge.category — the target category name (e.g. "animals")
131
+ // challenge.question — "Select all animals from the list (6 emojis, 3 correct)"
132
+ console.log(challenge.emojis); // ["🐶", "🍎", "🚗", "🐱", "🌸", "🏀"]
133
+ console.log(challenge.category); // "animals"
134
+
135
+ // user submits sorted comma-separated zero-based indices of the correct emojis
136
+ // e.g. if emojis[0] and emojis[3] are animals: "0,3"
137
+ const isValid = captcha.validate(challenge, "0,3");
138
+ if (isValid) {
139
+ console.log("Access granted!");
140
+ } else {
141
+ console.log("Wrong answer!");
142
+ }
115
143
  ```
116
144
 
145
+ Difficulty controls the number of emojis shown and correct answers required:
146
+
147
+ | Difficulty | Total emojis | Correct to select |
148
+ |------------|-------------|-------------------|
149
+ | easy | 4 | 2 |
150
+ | medium | 6 | 3 |
151
+ | hard | 8 | 4 |
152
+
153
+ 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"`.
154
+
117
155
  ### Mixed CAPTCHA
118
156
 
119
157
  ```typescript
@@ -129,9 +167,9 @@ const captcha = new K9Guard({ type: 'multi', difficulty: 'easy' });
129
167
  const challenge = captcha.generate();
130
168
 
131
169
  if (challenge.steps) {
132
- // user must solve both steps
133
- const answers = challenge.steps.map(step => step.answer.toString());
134
- const userInput = JSON.stringify(answers);
170
+ // user must solve both steps; steps expose only question/nonce/expiry — not the answer
171
+ // answers are submitted as a JSON array of strings
172
+ const userInput = JSON.stringify(["22", "typescript"]);
135
173
  const isValid = captcha.validate(challenge, userInput);
136
174
  }
137
175
  ```
@@ -160,9 +198,8 @@ const isValid = captcha.validate(challenge, "paris");
160
198
 
161
199
  ```typescript
162
200
  interface K9GuardOptions {
163
- type: 'math' | 'text' | 'riddle' | 'sequence' | 'scramble' | 'logic' | 'reverse' | 'mixed' | 'multi';
201
+ type: 'math' | 'text' | 'sequence' | 'scramble' | 'reverse' | 'mixed' | 'multi' | 'image' | 'emoji';
164
202
  difficulty: 'easy' | 'medium' | 'hard';
165
- locale?: 'en' | 'tr'; // default: 'en'
166
203
  }
167
204
  ```
168
205
 
@@ -185,18 +222,22 @@ interface CustomQuestion {
185
222
 
186
223
  #### `generate(): CaptchaChallenge`
187
224
 
188
- Generates a new CAPTCHA challenge with unique nonce, expiry time, and hashed answer.
225
+ 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
226
 
190
227
  ```typescript
191
228
  const challenge = captcha.generate();
192
- console.log(challenge.question); // the question to show user
193
- console.log(challenge.nonce); // unique session identifier
194
- console.log(challenge.expiry); // timestamp when challenge expires
229
+ console.log(challenge.question); // the question to show the user
230
+ console.log(challenge.nonce); // unique session identifier (pass back on validate)
231
+ console.log(challenge.expiry); // Unix ms timestamp when challenge expires
232
+ console.log(challenge.image); // base64 SVG data URI (only for type: 'image')
233
+ console.log(challenge.emojis); // emoji array (only for type: 'emoji')
234
+ console.log(challenge.category); // category name (only for type: 'emoji')
235
+ // challenge.answer / .hashedAnswer / .salt — NOT present; never sent to client
195
236
  ```
196
237
 
197
238
  #### `validate(challenge: CaptchaChallenge, userInput: string): boolean`
198
239
 
199
- Validates user input against the challenge. Returns `true` if correct, `false` otherwise.
240
+ 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.
200
241
 
201
242
  ```typescript
202
243
  const isValid = captcha.validate(challenge, userAnswer);
@@ -213,7 +254,6 @@ bun run src/test.ts
213
254
  Tests include:
214
255
  - All CAPTCHA types with correct/incorrect/edge case inputs
215
256
  - Custom question validation
216
- - Locale switching
217
257
  - Multi-step challenges
218
258
  - Input sanitization
219
259
  - Security validations
package/docs/tr/README.md CHANGED
@@ -4,14 +4,13 @@
4
4
 
5
5
  # K9Guard
6
6
 
7
- TypeScript/JavaScript projeleri için kriptografik güvenlik ve çok dilli destek sunan güvenli, hafif ve esnek bir CAPTCHA modülü.
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
- - **Çok Dilli Destek**: Bulmaca ve mantık soruları için İngilizce ve Türkçe dil desteği mevcuttur
13
- - **9 CAPTCHA Türü**: Matematik, metin, bulmaca, dizi, karıştırma, mantık, ters çevirme, karma ve çok adımlı doğrulama yöntemleri
14
- - **Güvenlik Odaklı**: SHA-256 tuzlu hash algoritması, nonce tabanlı oturum yönetimi ve 5 dakikalık geçerlilik süresi
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
15
14
  - **Girdi Doğrulama**: Enjeksiyon saldırılarını önlemek için uzunluk sınırlamaları, tip kontrolü ve sanitizasyon
16
15
  - **Özel Sorular**: Doğrulama ve sanitizasyon ile kendi sorularınızı tanımlama desteği
17
16
  - **Sıfır Bağımlılık**: Harici bağımlılık gerektirmeyen hafif yapı
@@ -32,8 +31,7 @@ import K9Guard from "k9guard";
32
31
 
33
32
  const captcha = new K9Guard({
34
33
  type: 'math',
35
- difficulty: 'medium',
36
- locale: 'tr'
34
+ difficulty: 'medium'
37
35
  });
38
36
 
39
37
  // doğrulama sorusu oluştur
@@ -69,15 +67,6 @@ const challenge = captcha.generate();
69
67
  // Cevap: "aB2xY9"
70
68
  ```
71
69
 
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
70
  ### Dizi CAPTCHA
82
71
 
83
72
  ```typescript
@@ -92,28 +81,77 @@ const challenge = captcha.generate();
92
81
  ```typescript
93
82
  const captcha = new K9Guard({ type: 'scramble', difficulty: 'easy' });
94
83
  const challenge = captcha.generate();
95
- // Çıktı: "iked"
96
- // Cevap: "kedi"
84
+ // Çıktı: "tac"
85
+ // Cevap: "cat"
97
86
  ```
98
87
 
99
- ### Mantık CAPTCHA
88
+ ### Ters Çevirme CAPTCHA
100
89
 
101
90
  ```typescript
102
- const captcha = new K9Guard({ type: 'logic', difficulty: 'easy', locale: 'tr' });
91
+ const captcha = new K9Guard({ type: 'reverse', difficulty: 'easy' });
103
92
  const challenge = captcha.generate();
104
- // Çıktı: "Su kuru bir maddedir. Doğru mu Yanlış mı?"
105
- // Cevap: "yanlış"
93
+ // Çıktı: "god"
94
+ // Cevap: "dog"
106
95
  ```
107
96
 
108
- ### Ters Çevirme CAPTCHA
97
+ ### Görsel CAPTCHA
109
98
 
110
99
  ```typescript
111
- const captcha = new K9Guard({ type: 'reverse', difficulty: 'easy' });
100
+ const captcha = new K9Guard({ type: 'image', difficulty: 'medium' });
101
+ const challenge = captcha.generate();
102
+
103
+ // challenge.image — doğrudan <img> etiketinde kullanılabilecek base64 SVG data URI
104
+ // challenge.question — "Type the characters shown in the image"
105
+ console.log(challenge.image); // "data:image/svg+xml;base64,..."
106
+
107
+ // kullanıcı yanıtını doğrula (büyük/küçük harf duyarsız)
108
+ const isValid = captcha.validate(challenge, "aB3z");
109
+ if (isValid) {
110
+ console.log("Erişim izni verildi!");
111
+ } else {
112
+ console.log("Yanlış cevap!");
113
+ }
114
+ ```
115
+
116
+ Görsel CAPTCHA'nın güvenlik özellikleri:
117
+ - **Karakter başına rotasyon ve offset** — rastgele renk ve boyutla OCR direnci
118
+ - **Sinüzoidal dalga katmanları** — zorluk seviyesine orantılı üst üste bindirilir
119
+ - **Gürültü çizgileri ve noktaları** — basit segmentasyon saldırılarını engeller
120
+ - **Büyük/küçük harf duyarsız doğrulama** — kullanıcı hem büyük hem küçük harf girebilir
121
+ - **Sıfır dış bağımlılık** — tamamen sunucu tarafında saf SVG ile üretilir
122
+
123
+ ### Emoji CAPTCHA
124
+
125
+ ```typescript
126
+ const captcha = new K9Guard({ type: 'emoji', difficulty: 'medium' });
112
127
  const challenge = captcha.generate();
113
- // Çıktı: "köpek"
114
- // Cevap: "kepök"
128
+
129
+ // challenge.emojis — gösterilecek emoji dizisi (medium için 6 adet)
130
+ // challenge.category — hedef kategori adı (örn. "animals")
131
+ // challenge.question — "Select all animals from the list (6 emojis, 3 correct)"
132
+ console.log(challenge.emojis); // ["🐶", "🍎", "🚗", "🐱", "🌸", "🏀"]
133
+ console.log(challenge.category); // "animals"
134
+
135
+ // kullanıcı, doğru emojilerin sıfır tabanlı indekslerini virgülle ayırarak gönderir
136
+ // örn. emojis[0] ve emojis[3] hayvan ise: "0,3"
137
+ const isValid = captcha.validate(challenge, "0,3");
138
+ if (isValid) {
139
+ console.log("Erişim izni verildi!");
140
+ } else {
141
+ console.log("Yanlış cevap!");
142
+ }
115
143
  ```
116
144
 
145
+ Zorluk seviyesi gösterilen emoji sayısını ve doğru seçilmesi gereken emoji sayısını belirler:
146
+
147
+ | Zorluk | Toplam emoji | Seçilmesi gereken |
148
+ |---------|-------------|-------------------|
149
+ | easy | 4 | 2 |
150
+ | medium | 6 | 3 |
151
+ | hard | 8 | 4 |
152
+
153
+ 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"`.
154
+
117
155
  ### Karma CAPTCHA
118
156
 
119
157
  ```typescript
@@ -129,9 +167,9 @@ const captcha = new K9Guard({ type: 'multi', difficulty: 'easy' });
129
167
  const challenge = captcha.generate();
130
168
 
131
169
  if (challenge.steps) {
132
- // kullanıcı her iki adımı da çözmelidir
133
- const answers = challenge.steps.map(step => step.answer.toString());
134
- const userInput = JSON.stringify(answers);
170
+ // kullanıcı her iki adımı da çözmelidir; steps yalnızca question/nonce/expiry içerir
171
+ // cevaplar JSON dizisi olarak gönderilir
172
+ const userInput = JSON.stringify(["22", "typescript"]);
135
173
  const isValid = captcha.validate(challenge, userInput);
136
174
  }
137
175
  ```
@@ -160,9 +198,8 @@ const isValid = captcha.validate(challenge, "ankara");
160
198
 
161
199
  ```typescript
162
200
  interface K9GuardOptions {
163
- type: 'math' | 'text' | 'riddle' | 'sequence' | 'scramble' | 'logic' | 'reverse' | 'mixed' | 'multi';
201
+ type: 'math' | 'text' | 'sequence' | 'scramble' | 'reverse' | 'mixed' | 'multi' | 'image' | 'emoji';
164
202
  difficulty: 'easy' | 'medium' | 'hard';
165
- locale?: 'en' | 'tr'; // varsayılan: 'en'
166
203
  }
167
204
  ```
168
205
 
@@ -185,18 +222,22 @@ interface CustomQuestion {
185
222
 
186
223
  #### `generate(): CaptchaChallenge`
187
224
 
188
- Benzersiz nonce, geçerlilik süresi ve hash'lenmiş cevap içeren yeni bir CAPTCHA doğrulaması oluşturur.
225
+ İ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
226
 
190
227
  ```typescript
191
228
  const challenge = captcha.generate();
192
- console.log(challenge.question); // kullanıcıya gösterilecek soru
193
- console.log(challenge.nonce); // benzersiz oturum tanımlayıcısı
194
- console.log(challenge.expiry); // doğrulamanın geçerlilik süresi sona erme zamanı
229
+ console.log(challenge.question); // kullanıcıya gösterilecek soru
230
+ console.log(challenge.nonce); // benzersiz oturum tanımlayıcısı (validate'e geri gönderilir)
231
+ console.log(challenge.expiry); // Unix ms cinsinden geçerlilik bitiş zamanı
232
+ console.log(challenge.image); // base64 SVG data URI (yalnızca type: 'image' için)
233
+ console.log(challenge.emojis); // emoji dizisi (yalnızca type: 'emoji' için)
234
+ console.log(challenge.category); // kategori adı (yalnızca type: 'emoji' için)
235
+ // challenge.answer / .hashedAnswer / .salt — MEVCUT DEĞİL; istemciye hiç gönderilmez
195
236
  ```
196
237
 
197
238
  #### `validate(challenge: CaptchaChallenge, userInput: string): boolean`
198
239
 
199
- Kullanıcı girdisini doğrulama ile karşılaştırır. Doğruysa `true`, yanlışsa `false` değeri döndürür.
240
+ 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.
200
241
 
201
242
  ```typescript
202
243
  const isValid = captcha.validate(challenge, userAnswer);
@@ -213,7 +254,6 @@ bun run src/test.ts
213
254
  Testler şunları içerir:
214
255
  - Tüm CAPTCHA türleri için doğru/yanlış/uç durum girdileri
215
256
  - Özel soru doğrulama senaryoları
216
- - Dil değiştirme işlemleri
217
257
  - Çok adımlı doğrulamalar
218
258
  - Girdi sanitizasyonu
219
259
  - Güvenlik doğrulamaları
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "k9guard",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
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.0.0",
51
- "@typescript-eslint/eslint-plugin": "^6.0.0",
52
- "@typescript-eslint/parser": "^6.0.0",
53
- "eslint": "^8.0.0",
54
- "tsx": "^4.0.0",
55
- "typescript": "^5.0.0"
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
@@ -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
- return { type: 'math', difficulty: 'medium', locale: 'en' };
18
+ return { type: 'math', difficulty: 'medium' };
19
19
  }
20
20
 
21
21
  const opt = options as Record<string, unknown>;
@@ -36,10 +36,12 @@ export class K9Guard {
36
36
  } as K9GuardCustomOptions;
37
37
  }
38
38
 
39
+ const validTypes = ['math', 'text', 'sequence', 'scramble', 'reverse', 'mixed', 'multi', 'image', 'emoji'];
40
+ const type = validTypes.includes(opt.type as string) ? opt.type : 'math';
41
+
39
42
  return {
40
- type: opt.type || 'math',
41
- difficulty: opt.difficulty || 'medium',
42
- locale: opt.locale || 'en'
43
+ type,
44
+ difficulty: opt.difficulty || 'medium'
43
45
  } as K9GuardOptions;
44
46
  }
45
47
 
@@ -56,12 +58,20 @@ export class K9Guard {
56
58
  return false;
57
59
  }
58
60
 
61
+ // resolve the stored record by nonce; reject if not found or expired.
62
+ // hashedAnswer and salt come from the server-side store, never from the client,
63
+ // which prevents hash-injection attacks.
64
+ const stored = this.generator.lookup(challenge.nonce);
65
+ if (!stored) {
66
+ return false;
67
+ }
68
+
59
69
  const now = Date.now();
60
- if (now > challenge.expiry) {
70
+ if (now > stored.expiry) {
61
71
  return false;
62
72
  }
63
73
 
64
- return CaptchaValidator.validate(challenge, userInput);
74
+ return CaptchaValidator.validate(stored, userInput);
65
75
  }
66
76
 
67
77
  private isValidChallenge(challenge: unknown): boolean {
@@ -74,10 +84,8 @@ export class K9Guard {
74
84
  return (
75
85
  typeof c.type === 'string' &&
76
86
  typeof c.question === 'string' &&
77
- typeof c.nonce === 'string' &&
78
- typeof c.expiry === 'number' &&
79
- typeof c.hashedAnswer === 'string' &&
80
- typeof c.salt === 'string'
87
+ typeof c.nonce === 'string' && c.nonce.length > 0 &&
88
+ typeof c.expiry === 'number'
81
89
  );
82
90
  }
83
91
  }