k9guard 1.0.0
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 +235 -0
- package/docs/tr/README.md +235 -0
- package/index.ts +4 -0
- package/package.json +64 -0
- package/src/K9Guard.ts +83 -0
- package/src/core/captchaGenerator.ts +233 -0
- package/src/core/captchaValidator.ts +117 -0
- package/src/locale/en.json +46 -0
- package/src/locale/tr.json +46 -0
- package/src/types/index.ts +72 -0
- package/src/utils/customQuestionGenerator.ts +40 -0
- package/src/utils/logicGenerator.ts +18 -0
- package/src/utils/random.ts +43 -0
- package/src/utils/reverseGenerator.ts +50 -0
- package/src/utils/riddleBank.ts +18 -0
- package/src/utils/scrambleGenerator.ts +37 -0
- package/src/utils/sequenceGenerator.ts +33 -0
- package/src/validators/customQuestionValidator.ts +88 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { randomBytes } from 'crypto';
|
|
2
|
+
import enData from '../locale/en.json';
|
|
3
|
+
import trData from '../locale/tr.json';
|
|
4
|
+
|
|
5
|
+
export class RiddleBank {
|
|
6
|
+
private static data: Record<'en' | 'tr', { question: string; answer: string }[]> = {
|
|
7
|
+
en: enData.riddles,
|
|
8
|
+
tr: trData.riddles
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
static getRandom(locale: 'en' | 'tr' = 'en', difficulty: 'easy' | 'medium' | 'hard' = 'easy'): { question: string; answer: string } {
|
|
12
|
+
const riddles = this.data[locale];
|
|
13
|
+
// use crypto random to select a riddle securely
|
|
14
|
+
const buffer = randomBytes(4);
|
|
15
|
+
const index = buffer.readUInt32LE(0) % riddles.length;
|
|
16
|
+
return riddles[index]!;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { randomBytes } from 'crypto';
|
|
2
|
+
|
|
3
|
+
export class ScrambleGenerator {
|
|
4
|
+
private static words: string[] = [
|
|
5
|
+
'apple', 'cat', 'dog', 'house', 'sun', 'moon', 'car', 'tree', 'book', 'water',
|
|
6
|
+
'bread', 'milk', 'fish', 'bird', 'flower', 'star', 'hand', 'eye', 'nose', 'mouth'
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
static generate(difficulty: 'easy' | 'medium' | 'hard'): { question: string; answer: string } {
|
|
10
|
+
let word: string;
|
|
11
|
+
const buffer = randomBytes(4);
|
|
12
|
+
// convert crypto bytes to a float between 0 and 1
|
|
13
|
+
const rand = buffer.readUInt32LE(0) / 0xFFFFFFFF;
|
|
14
|
+
|
|
15
|
+
if (difficulty === 'easy') {
|
|
16
|
+
word = this.words[Math.floor(rand * 10)]!;
|
|
17
|
+
} else if (difficulty === 'medium') {
|
|
18
|
+
word = this.words[Math.floor(rand * this.words.length)]!;
|
|
19
|
+
} else {
|
|
20
|
+
word = 'typescript';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const scrambled = this.scramble(word);
|
|
24
|
+
return { question: scrambled, answer: word };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private static scramble(word: string): string {
|
|
28
|
+
const arr = word.split('');
|
|
29
|
+
// NOTE: fisher yates shuffle using crypto random for each swap
|
|
30
|
+
for (let i = arr.length - 1; i > 0; i--) {
|
|
31
|
+
const buffer = randomBytes(4);
|
|
32
|
+
const j = buffer.readUInt32LE(0) % (i + 1);
|
|
33
|
+
[arr[i]!, arr[j]!] = [arr[j]!, arr[i]!];
|
|
34
|
+
}
|
|
35
|
+
return arr.join('');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { randomBytes } from 'crypto';
|
|
2
|
+
|
|
3
|
+
export class SequenceGenerator {
|
|
4
|
+
static generate(difficulty: 'easy' | 'medium' | 'hard'): { question: string; answer: number | string } {
|
|
5
|
+
if (difficulty === 'easy') {
|
|
6
|
+
const buffer = randomBytes(4);
|
|
7
|
+
// NOTE: generate random starting number and step size for arithmetic sequence
|
|
8
|
+
const start = (buffer.readUInt32LE(0) % 5) + 1;
|
|
9
|
+
const step = ((buffer.readUInt32LE(0) >> 8) % 3) + 1;
|
|
10
|
+
const sequence = [start, start + step, start + 2 * step];
|
|
11
|
+
const answer = start + 3 * step;
|
|
12
|
+
return { question: `${sequence.join(', ')}, ?`, answer };
|
|
13
|
+
}
|
|
14
|
+
if (difficulty === 'medium') {
|
|
15
|
+
const letters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'];
|
|
16
|
+
const buffer = randomBytes(4);
|
|
17
|
+
const start = buffer.readUInt32LE(0) % 4;
|
|
18
|
+
const step = 2;
|
|
19
|
+
const sequence = [letters[start], letters[start + step], letters[start + 2 * step]];
|
|
20
|
+
const nextIndex = start + 3 * step;
|
|
21
|
+
// check if next letter goes out of bounds
|
|
22
|
+
if (nextIndex >= letters.length) {
|
|
23
|
+
return { question: `${sequence.join(', ')}, ?`, answer: '?' };
|
|
24
|
+
}
|
|
25
|
+
const answer = letters[nextIndex]!;
|
|
26
|
+
return { question: `${sequence.join(', ')}, ?`, answer };
|
|
27
|
+
}
|
|
28
|
+
// hard mode uses fibonacci sequence
|
|
29
|
+
const sequence = [1, 1, 2, 3];
|
|
30
|
+
const answer = 5;
|
|
31
|
+
return { question: `${sequence.join(', ')}, ?`, answer };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { CustomQuestion } from '../types';
|
|
2
|
+
|
|
3
|
+
export class CustomQuestionValidator {
|
|
4
|
+
private static readonly MAX_QUESTIONS = 100;
|
|
5
|
+
private static readonly MAX_QUESTION_LENGTH = 500;
|
|
6
|
+
private static readonly MAX_ANSWER_LENGTH = 200;
|
|
7
|
+
private static readonly MIN_QUESTION_LENGTH = 5;
|
|
8
|
+
private static readonly MIN_ANSWER_LENGTH = 1;
|
|
9
|
+
private static readonly VALID_DIFFICULTY = ['easy', 'medium', 'hard'];
|
|
10
|
+
|
|
11
|
+
// validates the entire questions array structure and content
|
|
12
|
+
static validate(questions: unknown): { valid: boolean; error?: string } {
|
|
13
|
+
if (!Array.isArray(questions)) {
|
|
14
|
+
return { valid: false, error: 'Questions must be an array' };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (questions.length === 0) {
|
|
18
|
+
return { valid: false, error: 'At least one question is required' };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (questions.length > this.MAX_QUESTIONS) {
|
|
22
|
+
return { valid: false, error: `Maximum ${this.MAX_QUESTIONS} questions allowed` };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// NOTE: validate each question individually and return detailed error if any fail
|
|
26
|
+
for (let i = 0; i < questions.length; i++) {
|
|
27
|
+
const questionItem = questions[i];
|
|
28
|
+
const singleValidation = this.validateSingle(questionItem);
|
|
29
|
+
|
|
30
|
+
if (!singleValidation.valid) {
|
|
31
|
+
return { valid: false, error: `Question ${i + 1}: ${singleValidation.error}` };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { valid: true };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private static validateSingle(question: unknown): { valid: boolean; error?: string } {
|
|
39
|
+
if (typeof question !== 'object' || question === null) {
|
|
40
|
+
return { valid: false, error: 'Each question must be an object' };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const q = question as Record<string, unknown>;
|
|
44
|
+
|
|
45
|
+
if (typeof q.question !== 'string') {
|
|
46
|
+
return { valid: false, error: 'Question text must be a string' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (typeof q.answer !== 'string') {
|
|
50
|
+
return { valid: false, error: 'Answer must be a string' };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (typeof q.difficulty !== 'string') {
|
|
54
|
+
return { valid: false, error: 'Difficulty must be specified' };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!this.VALID_DIFFICULTY.includes(q.difficulty)) {
|
|
58
|
+
return { valid: false, error: `Difficulty must be one of: ${this.VALID_DIFFICULTY.join(', ')}` };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (q.question.length < this.MIN_QUESTION_LENGTH) {
|
|
62
|
+
return { valid: false, error: `Question must be at least ${this.MIN_QUESTION_LENGTH} characters` };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (q.question.length > this.MAX_QUESTION_LENGTH) {
|
|
66
|
+
return { valid: false, error: `Question must not exceed ${this.MAX_QUESTION_LENGTH} characters` };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (q.answer.length < this.MIN_ANSWER_LENGTH) {
|
|
70
|
+
return { valid: false, error: 'Answer cannot be empty' };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (q.answer.length > this.MAX_ANSWER_LENGTH) {
|
|
74
|
+
return { valid: false, error: `Answer must not exceed ${this.MAX_ANSWER_LENGTH} characters` };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { valid: true };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// clean up whitespace from questions and answers
|
|
81
|
+
static sanitize(questions: CustomQuestion[]): CustomQuestion[] {
|
|
82
|
+
return questions.map(q => ({
|
|
83
|
+
question: q.question.trim(),
|
|
84
|
+
answer: q.answer.trim(),
|
|
85
|
+
difficulty: q.difficulty
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Environment setup & latest features
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "Preserve",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
|
|
11
|
+
// Bundler mode
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
// Best practices
|
|
18
|
+
"strict": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
"noUncheckedIndexedAccess": true,
|
|
22
|
+
"noImplicitOverride": true,
|
|
23
|
+
|
|
24
|
+
// Some stricter flags (disabled by default)
|
|
25
|
+
"noUnusedLocals": false,
|
|
26
|
+
"noUnusedParameters": false,
|
|
27
|
+
"noPropertyAccessFromIndexSignature": false
|
|
28
|
+
}
|
|
29
|
+
}
|