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.
@@ -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
+ }