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 CHANGED
@@ -10,13 +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
15
  - **Single-Use Challenges**: Every nonce is consumed on the first `validate()` call — success or failure — preventing replay and brute-force attacks
15
16
  - **Strict Configuration**: Invalid `type` or `difficulty` values throw immediately; no silent fallbacks
16
17
  - **Input Validation**: Length limits, strict numeric parsing, type checking, and sanitization to prevent injection attacks
17
18
  - **Custom Questions**: Support for your own questions with validation and sanitization
18
19
  - **Zero Dependencies**: Lightweight with no external dependencies
19
- - **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
20
21
  - **OWASP Compliant**: Follows OWASP Top 10 security guidelines
21
22
  - **Privacy Compliant**: GDPR/KVKK compliant with no personal data storage
22
23
 
@@ -192,6 +193,101 @@ const challenge = captcha.generate();
192
193
  const isValid = captcha.validate(challenge, "paris");
193
194
  ```
194
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
+
195
291
  ## API Reference
196
292
 
197
293
  ### Constructor Options
@@ -203,7 +299,8 @@ Both `type` and `difficulty` are **required** and strictly validated. Passing an
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; // optional, required when difficulty is 'adaptive'
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
  ### Methods
226
324
 
227
- #### `generate(): CaptchaChallenge`
325
+ #### `generate(sessionId?: string): CaptchaChallenge`
228
326
 
229
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`.
230
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
+
231
331
  ```typescript
232
332
  const challenge = captcha.generate();
233
333
  console.log(challenge.question); // the question to show the user
@@ -239,10 +339,12 @@ console.log(challenge.category); // category name (only for type: 'emoji')
239
339
  // challenge.answer / .hashedAnswer / .salt — NOT present; never sent to client
240
340
  ```
241
341
 
242
- #### `validate(challenge: CaptchaChallenge, userInput: string): boolean`
342
+ #### `validate(challenge: CaptchaChallenge, userInput: string, sessionId?: string): boolean`
243
343
 
244
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.
245
345
 
346
+ When `difficulty` is `'adaptive'`, the validation result is automatically recorded for the session and the difficulty is adjusted accordingly.
347
+
246
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.
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
+ 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';
420
+ ```
421
+
422
+ ## Testing
423
+
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
427
+
428
+ ```bash
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
437
+ ```
438
+
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) |
448
+
258
449
  ## Contributing
259
450
 
260
451
  We welcome contributions! Here's how you can help:
@@ -262,7 +453,7 @@ We welcome contributions! Here's how you can help:
262
453
  1. **Fork the repository**
263
454
  2. **Create a feature branch**: `git checkout -b feature/amazing-feature`
264
455
  3. **Add tests** for your changes
265
- 4. **Run tests**: `bun run src/test.ts`
456
+ 4. **Run tests**: `bun test`
266
457
  5. **Commit your changes**: `git commit -m 'feat: add amazing feature'`
267
458
  6. **Push to branch**: `git push origin feature/amazing-feature`
268
459
  7. **Open a Pull Request**