k9guard 1.0.2 → 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 CHANGED
@@ -10,11 +10,14 @@ A secure, lightweight, and flexible CAPTCHA module for TypeScript/JavaScript pro
10
10
 
11
11
  - **Cryptographically Secure**: NIST SP 800-90A compliant random generation
12
12
  - **10 CAPTCHA Types**: Math, text, sequence, scramble, reverse, mixed, multi-step, image, emoji, and custom challenges
13
+ - **Adaptive Difficulty**: Automatically adjusts challenge difficulty based on user success rate
13
14
  - **Security First**: SHA-256 salted hashing, server-side challenge store, nonce-based session management, and 5-minute expiry
14
- - **Input Validation**: Length limits, type checking, and sanitization to prevent injection attacks
15
+ - **Single-Use Challenges**: Every nonce is consumed on the first `validate()` call — success or failure — preventing replay and brute-force attacks
16
+ - **Strict Configuration**: Invalid `type` or `difficulty` values throw immediately; no silent fallbacks
17
+ - **Input Validation**: Length limits, strict numeric parsing, type checking, and sanitization to prevent injection attacks
15
18
  - **Custom Questions**: Support for your own questions with validation and sanitization
16
19
  - **Zero Dependencies**: Lightweight with no external dependencies
17
- - **Well Tested**: Comprehensive test coverage including edge cases and security scenarios
20
+ - **Well Tested**: 228+ tests covering unit, integration, security, edge-case, and benchmark scenarios
18
21
  - **OWASP Compliant**: Follows OWASP Top 10 security guidelines
19
22
  - **Privacy Compliant**: GDPR/KVKK compliant with no personal data storage
20
23
 
@@ -190,16 +193,114 @@ const challenge = captcha.generate();
190
193
  const isValid = captcha.validate(challenge, "paris");
191
194
  ```
192
195
 
196
+ ### Adaptive Difficulty
197
+
198
+ Adaptive difficulty automatically adjusts the challenge difficulty based on the user's success rate. This provides a better user experience — easy challenges for struggling users and harder challenges for those who solve quickly.
199
+
200
+ #### How It Works
201
+
202
+ - Tracks the last 10 attempts per session (sliding window)
203
+ - **80%+ success rate** — difficulty increases (easy -> medium -> hard)
204
+ - **40% or lower success rate** — difficulty decreases (hard -> medium -> easy)
205
+ - **40-80% success rate** — difficulty stays stable
206
+ - Minimum 3 attempts required before any adjustment (hysteresis)
207
+ - Sessions expire after 30 minutes of inactivity
208
+ - Maximum 10,000 concurrent sessions (oldest evicted automatically)
209
+
210
+ #### Option 1: Session ID in Constructor
211
+
212
+ ```typescript
213
+ const captcha = new K9Guard({
214
+ type: 'math',
215
+ difficulty: 'adaptive',
216
+ sessionId: 'user-123' // any unique string (user ID, session token, IP, etc.)
217
+ });
218
+
219
+ const challenge = captcha.generate(); // uses user-123's current difficulty
220
+ const isValid = captcha.validate(challenge, userAnswer); // records result automatically
221
+ ```
222
+
223
+ #### Option 2: Session ID as Parameter
224
+
225
+ ```typescript
226
+ const captcha = new K9Guard({ type: 'math', difficulty: 'adaptive' });
227
+
228
+ // pass sessionId with each call
229
+ const challenge = captcha.generate('user-123');
230
+ const isValid = captcha.validate(challenge, userAnswer, 'user-123');
231
+ ```
232
+
233
+ #### Option 3: Flexible (Both)
234
+
235
+ Constructor `sessionId` is the default. Parameter `sessionId` overrides it.
236
+
237
+ ```typescript
238
+ const captcha = new K9Guard({
239
+ type: 'math',
240
+ difficulty: 'adaptive',
241
+ sessionId: 'default-user'
242
+ });
243
+
244
+ captcha.generate(); // uses 'default-user'
245
+ captcha.generate('other-user'); // uses 'other-user' (overrides default)
246
+ ```
247
+
248
+ #### Session Management
249
+
250
+ ```typescript
251
+ // get current difficulty for a session
252
+ const difficulty = captcha.getSessionDifficulty('user-123'); // 'easy' | 'medium' | 'hard' | null
253
+
254
+ // clear a specific session (resets to 'medium')
255
+ captcha.clearSession('user-123');
256
+
257
+ // clear all sessions
258
+ captcha.clearAllSessions();
259
+ ```
260
+
261
+ #### Works with All CAPTCHA Types
262
+
263
+ ```typescript
264
+ // adaptive works with any captcha type
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
+ // ... and so on
269
+ ```
270
+
271
+ #### Express.js Example
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
+
193
291
  ## API Reference
194
292
 
195
293
  ### Constructor Options
196
294
 
295
+ Both `type` and `difficulty` are **required** and strictly validated. Passing an invalid value throws an error immediately.
296
+
197
297
  #### Standard CAPTCHA Options
198
298
 
199
299
  ```typescript
200
300
  interface K9GuardOptions {
201
301
  type: 'math' | 'text' | 'sequence' | 'scramble' | 'reverse' | 'mixed' | 'multi' | 'image' | 'emoji';
202
- difficulty: 'easy' | 'medium' | 'hard';
302
+ difficulty: 'easy' | 'medium' | 'hard' | 'adaptive';
303
+ sessionId?: string; // optional, required when difficulty is 'adaptive'
203
304
  }
204
305
  ```
205
306
 
@@ -209,6 +310,7 @@ interface K9GuardOptions {
209
310
  interface K9GuardCustomOptions {
210
311
  type: 'custom';
211
312
  questions: CustomQuestion[];
313
+ sessionId?: string;
212
314
  }
213
315
 
214
316
  interface CustomQuestion {
@@ -220,10 +322,12 @@ interface CustomQuestion {
220
322
 
221
323
  ### Methods
222
324
 
223
- #### `generate(): CaptchaChallenge`
325
+ #### `generate(sessionId?: string): CaptchaChallenge`
224
326
 
225
327
  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`.
226
328
 
329
+ When `difficulty` is `'adaptive'`, the `sessionId` parameter is used to look up the user's current difficulty level. If a `sessionId` was provided in the constructor, it is used as the default.
330
+
227
331
  ```typescript
228
332
  const challenge = captcha.generate();
229
333
  console.log(challenge.question); // the question to show the user
@@ -235,28 +339,112 @@ console.log(challenge.category); // category name (only for type: 'emoji')
235
339
  // challenge.answer / .hashedAnswer / .salt — NOT present; never sent to client
236
340
  ```
237
341
 
238
- #### `validate(challenge: CaptchaChallenge, userInput: string): boolean`
342
+ #### `validate(challenge: CaptchaChallenge, userInput: string, sessionId?: string): boolean`
239
343
 
240
344
  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.
241
345
 
346
+ When `difficulty` is `'adaptive'`, the validation result is automatically recorded for the session and the difficulty is adjusted accordingly.
347
+
348
+ > **⚠️ 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.
349
+
242
350
  ```typescript
243
351
  const isValid = captcha.validate(challenge, userAnswer);
352
+
353
+ // After validate(), the challenge is consumed.
354
+ // For a retry, generate a fresh challenge:
355
+ if (!isValid) {
356
+ const newChallenge = captcha.generate();
357
+ }
358
+ ```
359
+
360
+ #### `getSessionDifficulty(sessionId: string): Difficulty | null`
361
+
362
+ Returns the current adaptive difficulty for a session. Returns `null` if the instance is not in adaptive mode.
363
+
364
+ ```typescript
365
+ const difficulty = captcha.getSessionDifficulty('user-123');
366
+ // 'easy' | 'medium' | 'hard' | null
367
+ ```
368
+
369
+ #### `clearSession(sessionId: string): boolean`
370
+
371
+ Removes a specific adaptive session. Returns `true` if the session existed, `false` otherwise.
372
+
373
+ ```typescript
374
+ captcha.clearSession('user-123');
375
+ ```
376
+
377
+ #### `clearAllSessions(): void`
378
+
379
+ Removes all adaptive sessions.
380
+
381
+ ```typescript
382
+ captcha.clearAllSessions();
383
+ ```
384
+
385
+ ### Exported Utilities
386
+
387
+ ```typescript
388
+ import K9Guard, { AdaptiveTracker, CustomQuestionValidator, CustomQuestionGenerator } from 'k9guard';
389
+ ```
390
+
391
+ | Export | Description |
392
+ |--------|-------------|
393
+ | `K9Guard` (default) | Main CAPTCHA class |
394
+ | `AdaptiveTracker` | Standalone adaptive difficulty tracker (useful for custom integrations) |
395
+ | `CustomQuestionValidator` | Validate and sanitize custom question arrays |
396
+ | `CustomQuestionGenerator` | Generate from custom question pools |
397
+
398
+ ### Type Exports
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';
244
420
  ```
245
421
 
246
422
  ## Testing
247
423
 
248
- Run the included test suite:
424
+ K9Guard uses `bun:test` for its test suite with 228+ tests covering unit, integration, security, edge-case, and benchmark scenarios.
425
+
426
+ ### Run Tests
249
427
 
250
428
  ```bash
251
- bun run src/test.ts
429
+ # run all tests
430
+ bun test
431
+
432
+ # run tests in watch mode
433
+ bun run test:watch
434
+
435
+ # run tests with coverage
436
+ bun run test:coverage
252
437
  ```
253
438
 
254
- Tests include:
255
- - All CAPTCHA types with correct/incorrect/edge case inputs
256
- - Custom question validation
257
- - Multi-step challenges
258
- - Input sanitization
259
- - Security validations
439
+ ### Test Categories
440
+
441
+ | Category | Coverage |
442
+ |----------|----------|
443
+ | **Unit** | Each module tested independently with correct outputs and edge cases |
444
+ | **Integration** | Full generate-validate flow for all 10 captcha types + adaptive mode |
445
+ | **Security** | Timing attack resistance, nonce replay prevention, hash injection, input sanitization, SVG injection |
446
+ | **Edge Cases** | Division by zero, unicode characters, concurrent generators, invalid inputs |
447
+ | **Benchmark** | Performance assertions for all captcha types (generate < 5ms, validate < 5ms) |
260
448
 
261
449
  ## Contributing
262
450
 
@@ -265,7 +453,7 @@ We welcome contributions! Here's how you can help:
265
453
  1. **Fork the repository**
266
454
  2. **Create a feature branch**: `git checkout -b feature/amazing-feature`
267
455
  3. **Add tests** for your changes
268
- 4. **Run tests**: `bun run src/test.ts`
456
+ 4. **Run tests**: `bun test`
269
457
  5. **Commit your changes**: `git commit -m 'feat: add amazing feature'`
270
458
  6. **Push to branch**: `git push origin feature/amazing-feature`
271
459
  7. **Open a Pull Request**