k9guard 1.0.1 → 1.0.3

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,154 @@
1
+ import { RandomPool } from './crypto';
2
+
3
+ interface ImageGeneratorResult {
4
+ image: string;
5
+ answer: string;
6
+ question: string;
7
+ }
8
+
9
+ const CHAR_POOL_EASY = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
10
+ const CHAR_POOL_MEDIUM = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
11
+ const CHAR_POOL_HARD = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789';
12
+
13
+ const SVG_WIDTH = 200;
14
+ const SVG_HEIGHT = 70;
15
+
16
+ // prevents XML injection via user-controlled strings embedded in SVG
17
+ function escapeXml(str: string): string {
18
+ return str
19
+ .replace(/&/g, '&')
20
+ .replace(/</g, '&lt;')
21
+ .replace(/>/g, '&gt;')
22
+ .replace(/"/g, '&quot;')
23
+ .replace(/'/g, '&#39;');
24
+ }
25
+
26
+ function generateText(pool: RandomPool, charPool: string, length: number): string {
27
+ let result = '';
28
+ for (let i = 0; i < length; i++) {
29
+ result += charPool[pool.byte() % charPool.length]!;
30
+ }
31
+ return result;
32
+ }
33
+
34
+ // each character is rendered individually with rotation and offset to resist OCR
35
+ function renderChar(pool: RandomPool, char: string, x: number, baseY: number, colorHex: string): string {
36
+ const rotate = pool.int(-25, 25);
37
+ const offsetY = pool.int(-6, 6);
38
+ const fontSize = pool.int(22, 30);
39
+ const opacity = (pool.float() * 0.25 + 0.75).toFixed(2);
40
+
41
+ return `<text x="${x}" y="${baseY + offsetY}" transform="rotate(${rotate},${x},${baseY + offsetY})" font-size="${fontSize}" font-family="monospace" font-weight="bold" fill="${colorHex}" opacity="${opacity}" letter-spacing="2">${escapeXml(char)}</text>`;
42
+ }
43
+
44
+ function grayColor(pool: RandomPool): string {
45
+ const v = pool.int(100, 200);
46
+ return `rgb(${v},${v},${v})`;
47
+ }
48
+
49
+ function darkColor(pool: RandomPool): string {
50
+ const r = pool.int(20, 150);
51
+ const g = pool.int(20, 150);
52
+ const b = pool.int(20, 150);
53
+ return `rgb(${r},${g},${b})`;
54
+ }
55
+
56
+ // interference lines make automated segment extraction harder
57
+ function renderNoiseLines(pool: RandomPool, count: number): string {
58
+ let lines = '';
59
+ for (let i = 0; i < count; i++) {
60
+ const x1 = pool.int(0, SVG_WIDTH);
61
+ const y1 = pool.int(0, SVG_HEIGHT);
62
+ const x2 = pool.int(0, SVG_WIDTH);
63
+ const y2 = pool.int(0, SVG_HEIGHT);
64
+ const strokeWidth = (pool.float() * 1.5 + 0.5).toFixed(1);
65
+ const color = grayColor(pool);
66
+ const opacity = (pool.float() * 0.4 + 0.2).toFixed(2);
67
+ lines += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${color}" stroke-width="${strokeWidth}" opacity="${opacity}"/>`;
68
+ }
69
+ return lines;
70
+ }
71
+
72
+ // noise dots add pixel-level entropy that defeats simple segmentation
73
+ function renderNoiseDots(pool: RandomPool, count: number): string {
74
+ let dots = '';
75
+ for (let i = 0; i < count; i++) {
76
+ const cx = pool.int(0, SVG_WIDTH);
77
+ const cy = pool.int(0, SVG_HEIGHT);
78
+ const r = pool.int(1, 3);
79
+ const color = grayColor(pool);
80
+ const opacity = (pool.float() * 0.5 + 0.1).toFixed(2);
81
+ dots += `<circle cx="${cx}" cy="${cy}" r="${r}" fill="${color}" opacity="${opacity}"/>`;
82
+ }
83
+ return dots;
84
+ }
85
+
86
+ // sinusoidal wave distortion applied as a path overlay
87
+ function renderWavePath(pool: RandomPool): string {
88
+ const amplitude = pool.int(3, 8);
89
+ const frequency = pool.float() * 0.08 + 0.04;
90
+ const phaseShift = pool.float() * Math.PI * 2;
91
+ const strokeColor = grayColor(pool);
92
+
93
+ let d = `M 0 ${SVG_HEIGHT / 2}`;
94
+ for (let x = 1; x <= SVG_WIDTH; x += 2) {
95
+ const y = SVG_HEIGHT / 2 + amplitude * Math.sin(frequency * x + phaseShift);
96
+ d += ` L ${x} ${y.toFixed(2)}`;
97
+ }
98
+
99
+ return `<path d="${d}" stroke="${strokeColor}" stroke-width="1.5" fill="none" opacity="0.35"/>`;
100
+ }
101
+
102
+ function buildSvg(pool: RandomPool, text: string, difficulty: 'easy' | 'medium' | 'hard'): string {
103
+ const charCount = text.length;
104
+ const spacing = SVG_WIDTH / (charCount + 1);
105
+ const baseY = SVG_HEIGHT / 2 + 8;
106
+
107
+ const noiseLineCount = difficulty === 'easy' ? 4 : difficulty === 'medium' ? 7 : 10;
108
+ const noiseDotCount = difficulty === 'easy' ? 20 : difficulty === 'medium' ? 40 : 60;
109
+ const waveCount = difficulty === 'easy' ? 1 : difficulty === 'medium' ? 2 : 3;
110
+
111
+ const bgGray = pool.int(235, 250);
112
+ const background = `<rect width="${SVG_WIDTH}" height="${SVG_HEIGHT}" fill="rgb(${bgGray},${bgGray},${bgGray})" rx="6"/>`;
113
+
114
+ let chars = '';
115
+ for (let i = 0; i < charCount; i++) {
116
+ const x = Math.round(spacing * (i + 1));
117
+ const color = darkColor(pool);
118
+ chars += renderChar(pool, text[i]!, x, baseY, color);
119
+ }
120
+
121
+ let waves = '';
122
+ for (let i = 0; i < waveCount; i++) {
123
+ waves += renderWavePath(pool);
124
+ }
125
+
126
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${SVG_WIDTH}" height="${SVG_HEIGHT}" viewBox="0 0 ${SVG_WIDTH} ${SVG_HEIGHT}">${background}${renderNoiseDots(pool, noiseDotCount)}${renderNoiseLines(pool, noiseLineCount)}${waves}${chars}</svg>`;
127
+ }
128
+
129
+ function svgToDataUri(svg: string): string {
130
+ // btoa requires a binary string; TextEncoder gives us the UTF-8 bytes
131
+ const bytes = new TextEncoder().encode(svg);
132
+ const binString = Array.from(bytes, b => String.fromCodePoint(b)).join('');
133
+ return `data:image/svg+xml;base64,${btoa(binString)}`;
134
+ }
135
+
136
+ export class ImageGenerator {
137
+ static generate(difficulty: 'easy' | 'medium' | 'hard'): ImageGeneratorResult {
138
+ // single pool allocation covers all random needs for this image — ~1 crypto
139
+ // call instead of the ~500 individual randomBytes(4) calls it replaced
140
+ const pool = new RandomPool(2048);
141
+
142
+ const charPool = difficulty === 'easy' ? CHAR_POOL_EASY : difficulty === 'medium' ? CHAR_POOL_MEDIUM : CHAR_POOL_HARD;
143
+ const length = difficulty === 'easy' ? 4 : difficulty === 'medium' ? 5 : 6;
144
+ const answer = generateText(pool, charPool, length);
145
+ const svg = buildSvg(pool, answer, difficulty);
146
+ const image = svgToDataUri(svg);
147
+
148
+ return {
149
+ image,
150
+ answer: answer.toLowerCase(),
151
+ question: 'Type the characters shown in the image'
152
+ };
153
+ }
154
+ }
@@ -3,8 +3,8 @@ import { randomBytes } from './crypto';
3
3
  export class Random {
4
4
  static getRandomNumber(difficulty: 'easy' | 'medium' | 'hard'): number {
5
5
  const buffer = randomBytes(4);
6
- // NOTE: convert crypto bytes to a number between 0 and 1
7
- const rand = buffer.readUInt32LE(0) / 0xFFFFFFFF;
6
+ // divide by 2^32 (not 2^32-1) so the result is strictly in [0, 1) — avoids off-by-one at the top
7
+ const rand = buffer.readUInt32LE(0) / 0x100000000;
8
8
  if (difficulty === 'easy') {
9
9
  return Math.floor(rand * 10) + 1;
10
10
  }
@@ -1,3 +1,5 @@
1
+ import { randomBytes } from './crypto';
2
+
1
3
  export class ReverseGenerator {
2
4
  private static readonly easyWords = [
3
5
  'cat', 'dog', 'sun', 'moon', 'star', 'fish', 'bird', 'tree',
@@ -42,7 +44,9 @@ export class ReverseGenerator {
42
44
  wordPool = this.hardWords;
43
45
  }
44
46
 
45
- const text = wordPool![Math.floor(Math.random() * wordPool!.length)]!;
47
+ const buf = randomBytes(4);
48
+ const rand = buf.readUInt32LE(0) / 0xFFFFFFFF;
49
+ const text = wordPool![Math.floor(rand * wordPool!.length)]!;
46
50
  const reversed = text.split('').reverse().join('');
47
51
 
48
52
  return { question: reversed, answer: text };
@@ -1,24 +1,35 @@
1
1
  import { randomBytes } from './crypto';
2
2
 
3
3
  export class ScrambleGenerator {
4
- private static words: string[] = [
4
+ private static readonly easyWords: string[] = [
5
+ 'apple', 'cat', 'dog', 'house', 'sun', 'moon', 'car', 'tree', 'book', 'water'
6
+ ];
7
+
8
+ private static readonly mediumWords: string[] = [
5
9
  'apple', 'cat', 'dog', 'house', 'sun', 'moon', 'car', 'tree', 'book', 'water',
6
10
  'bread', 'milk', 'fish', 'bird', 'flower', 'star', 'hand', 'eye', 'nose', 'mouth'
7
11
  ];
8
12
 
13
+ // hard pool: longer words with uncommon letter patterns to resist guessing
14
+ private static readonly hardWords: string[] = [
15
+ 'typescript', 'javascript', 'algorithm', 'encryption', 'blockchain',
16
+ 'metamorphosis', 'cryptography', 'synchronize', 'parallelism', 'obfuscation',
17
+ 'infrastructure', 'authentication', 'authorization', 'vulnerability', 'orchestration'
18
+ ];
19
+
9
20
  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
-
21
+ let pool: string[];
15
22
  if (difficulty === 'easy') {
16
- word = this.words[Math.floor(rand * 10)]!;
23
+ pool = this.easyWords;
17
24
  } else if (difficulty === 'medium') {
18
- word = this.words[Math.floor(rand * this.words.length)]!;
25
+ pool = this.mediumWords;
19
26
  } else {
20
- word = 'typescript';
27
+ pool = this.hardWords;
21
28
  }
29
+
30
+ const buffer = randomBytes(4);
31
+ const rand = buffer.readUInt32LE(0) / 0xFFFFFFFF;
32
+ const word = pool[Math.floor(rand * pool.length)]!;
22
33
 
23
34
  const scrambled = this.scramble(word);
24
35
  return { question: scrambled, answer: word };
@@ -26,10 +37,11 @@ export class ScrambleGenerator {
26
37
 
27
38
  private static scramble(word: string): string {
28
39
  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);
40
+ const n = arr.length;
41
+ // batch all uint32 values for Fisher-Yates in one crypto call (n-1 swaps × 4 bytes)
42
+ const batchBuf = randomBytes(n * 4);
43
+ for (let i = n - 1; i > 0; i--) {
44
+ const j = batchBuf.readUInt32LE((n - 1 - i) * 4) % (i + 1);
33
45
  [arr[i]!, arr[j]!] = [arr[j]!, arr[i]!];
34
46
  }
35
47
  return arr.join('');
@@ -3,31 +3,32 @@ import { randomBytes } from './crypto';
3
3
  export class SequenceGenerator {
4
4
  static generate(difficulty: 'easy' | 'medium' | 'hard'): { question: string; answer: number | string } {
5
5
  if (difficulty === 'easy') {
6
- const buffer = randomBytes(4);
7
- // NOTE: generate random starting number and step size for arithmetic sequence
6
+ const buffer = randomBytes(8);
7
+ // use separate byte ranges for start and step to remove correlation between them
8
8
  const start = (buffer.readUInt32LE(0) % 5) + 1;
9
- const step = ((buffer.readUInt32LE(0) >> 8) % 3) + 1;
9
+ const step = (buffer.readUInt32LE(4) % 3) + 1;
10
10
  const sequence = [start, start + step, start + 2 * step];
11
11
  const answer = start + 3 * step;
12
12
  return { question: `${sequence.join(', ')}, ?`, answer };
13
13
  }
14
14
  if (difficulty === 'medium') {
15
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
16
  const step = 2;
17
+ // max start ensures all 4 elements (start, +2, +4, +6) fit within the 10-letter array
18
+ const maxStart = letters.length - step * 3 - 1;
19
+ const buffer = randomBytes(4);
20
+ const start = buffer.readUInt32LE(0) % (maxStart + 1);
19
21
  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]!;
22
+ const answer = letters[start + 3 * step]!;
26
23
  return { question: `${sequence.join(', ')}, ?`, answer };
27
24
  }
28
- // hard mode uses fibonacci sequence
29
- const sequence = [1, 1, 2, 3];
30
- const answer = 5;
25
+ // hard: pick a random starting offset in the Fibonacci sequence so the answer is not always 5
26
+ const fibs = [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144];
27
+ const buffer = randomBytes(4);
28
+ const maxStart = fibs.length - 5; // ensure 4 shown + 1 answer all fit
29
+ const start = buffer.readUInt32LE(0) % (maxStart + 1);
30
+ const sequence = fibs.slice(start, start + 4);
31
+ const answer = fibs[start + 4]!;
31
32
  return { question: `${sequence.join(', ')}, ?`, answer };
32
33
  }
33
34
  }
@@ -1,46 +0,0 @@
1
- {
2
- "riddles": [
3
- { "question": "What has keys but can't open locks?", "answer": "piano" },
4
- { "question": "What gets wetter as it dries?", "answer": "towel" },
5
- { "question": "What has a head, a tail, but no body?", "answer": "coin" },
6
- { "question": "What can you catch but not throw?", "answer": "cold" },
7
- { "question": "What has one eye but can't see?", "answer": "needle" },
8
- { "question": "What is always in front of you but can't be seen?", "answer": "future" },
9
- { "question": "What has many teeth but can't bite?", "answer": "comb" },
10
- { "question": "What is full of holes but still holds water?", "answer": "sponge" },
11
- { "question": "What has a neck but no head?", "answer": "bottle" },
12
- { "question": "What can travel around the world while staying in a corner?", "answer": "stamp" },
13
- { "question": "What has hands but cannot clap?", "answer": "clock" },
14
- { "question": "What runs but never walks?", "answer": "water" },
15
- { "question": "What has a bed but never sleeps?", "answer": "river" },
16
- { "question": "What has a face and two hands but no arms or legs?", "answer": "clock" },
17
- { "question": "What goes up but never comes down?", "answer": "age" },
18
- { "question": "What has words but never speaks?", "answer": "book" },
19
- { "question": "What has a thumb and four fingers but is not alive?", "answer": "glove" },
20
- { "question": "What can fill a room but takes up no space?", "answer": "light" },
21
- { "question": "What breaks when you say its name?", "answer": "silence" },
22
- { "question": "What has legs but cannot walk?", "answer": "table" }
23
- ],
24
- "logics": [
25
- { "question": "If today is Monday, tomorrow is Tuesday. True or False?", "answer": "True" },
26
- { "question": "A cat is an animal. True or False?", "answer": "True" },
27
- { "question": "Water is dry. True or False?", "answer": "False" },
28
- { "question": "All apples are red. True or False?", "answer": "False" },
29
- { "question": "5 is greater than 3. True or False?", "answer": "True" },
30
- { "question": "If A > B and B > C, then A > C. True or False?", "answer": "True" },
31
- { "question": "A square has 3 sides. True or False?", "answer": "False" },
32
- { "question": "The sun rises in the east. True or False?", "answer": "True" },
33
- { "question": "Fish can fly. True or False?", "answer": "False" },
34
- { "question": "2 + 2 = 4. True or False?", "answer": "True" },
35
- { "question": "A triangle has 3 corners. True or False?", "answer": "True" },
36
- { "question": "Ice is hot. True or False?", "answer": "False" },
37
- { "question": "10 is an even number. True or False?", "answer": "True" },
38
- { "question": "Humans need oxygen to breathe. True or False?", "answer": "True" },
39
- { "question": "A year has 12 months. True or False?", "answer": "True" },
40
- { "question": "Birds are mammals. True or False?", "answer": "False" },
41
- { "question": "7 is less than 15. True or False?", "answer": "True" },
42
- { "question": "A circle has corners. True or False?", "answer": "False" },
43
- { "question": "The earth is flat. True or False?", "answer": "False" },
44
- { "question": "A week has 7 days. True or False?", "answer": "True" }
45
- ]
46
- }
@@ -1,46 +0,0 @@
1
- {
2
- "riddles": [
3
- { "question": "Anahtarları var ama kapıları açamaz?", "answer": "piyano" },
4
- { "question": "Kurudukça ıslanan nedir?", "answer": "havlu" },
5
- { "question": "Baş ve kuyruğu var ama vücudu yok?", "answer": "madeni para" },
6
- { "question": "Yakalayabilirsin ama atamazsın?", "answer": "soğuk algınlığı" },
7
- { "question": "Bir gözü var ama göremez?", "answer": "iğne" },
8
- { "question": "Her zaman önünde ama görülemez?", "answer": "gelecek" },
9
- { "question": "Çok dişleri var ama ısırmaz?", "answer": "tarak" },
10
- { "question": "Delik dolu ama suyu tutar?", "answer": "sünger" },
11
- { "question": "Boynu var ama başı yok?", "answer": "şişe" },
12
- { "question": "Dünyayı dolaşır ama köşede kalır?", "answer": "posta pulu" },
13
- { "question": "Elleri var ama alkışlayamaz?", "answer": "saat" },
14
- { "question": "Koşar ama asla yürümez?", "answer": "su" },
15
- { "question": "Yatağı var ama asla uyumaz?", "answer": "nehir" },
16
- { "question": "Yüzü ve iki eli var ama kolu ve bacağı yok?", "answer": "saat" },
17
- { "question": "Yukarı çıkar ama asla aşağı inmez?", "answer": "yaş" },
18
- { "question": "Kelimeleri var ama asla konuşmaz?", "answer": "kitap" },
19
- { "question": "Başparmağı ve dört parmağı var ama canlı değil?", "answer": "eldiven" },
20
- { "question": "Bir odayı doldurur ama yer kaplamaz?", "answer": "ışık" },
21
- { "question": "Adını söylediğinde kırılır?", "answer": "sessizlik" },
22
- { "question": "Bacakları var ama yürüyemez?", "answer": "masa" }
23
- ],
24
- "logics": [
25
- { "question": "Bugün Pazartesi ise, yarın Salı'dır. Doğru mu Yanlış mı?", "answer": "Doğru" },
26
- { "question": "Kedi bir hayvandır. Doğru mu Yanlış mı?", "answer": "Doğru" },
27
- { "question": "Su kurudur. Doğru mu Yanlış mı?", "answer": "Yanlış" },
28
- { "question": "Tüm elmalar kırmızıdır. Doğru mu Yanlış mı?", "answer": "Yanlış" },
29
- { "question": "5, 3'ten büyüktür. Doğru mu Yanlış mı?", "answer": "Doğru" },
30
- { "question": "Eğer A > B ve B > C ise, A > C olur. Doğru mu Yanlış mı?", "answer": "Doğru" },
31
- { "question": "Karenin 3 kenarı vardır. Doğru mu Yanlış mı?", "answer": "Yanlış" },
32
- { "question": "Güneş doğudan doğar. Doğru mu Yanlış mı?", "answer": "Doğru" },
33
- { "question": "Balıklar uçar. Doğru mu Yanlış mı?", "answer": "Yanlış" },
34
- { "question": "2 + 2 = 4. Doğru mu Yanlış mı?", "answer": "Doğru" },
35
- { "question": "Üçgenin 3 köşesi vardır. Doğru mu Yanlış mı?", "answer": "Doğru" },
36
- { "question": "Buz sıcaktır. Doğru mu Yanlış mı?", "answer": "Yanlış" },
37
- { "question": "10 çift sayıdır. Doğru mu Yanlış mı?", "answer": "Doğru" },
38
- { "question": "İnsanlar nefes almak için oksijene ihtiyaç duyar. Doğru mu Yanlış mı?", "answer": "Doğru" },
39
- { "question": "Bir yılda 12 ay vardır. Doğru mu Yanlış mı?", "answer": "Doğru" },
40
- { "question": "Kuşlar memelidir. Doğru mu Yanlış mı?", "answer": "Yanlış" },
41
- { "question": "7, 15'ten küçüktür. Doğru mu Yanlış mı?", "answer": "Doğru" },
42
- { "question": "Dairenin köşeleri vardır. Doğru mu Yanlış mı?", "answer": "Yanlış" },
43
- { "question": "Dünya düzdür. Doğru mu Yanlış mı?", "answer": "Yanlış" },
44
- { "question": "Bir haftada 7 gün vardır. Doğru mu Yanlış mı?", "answer": "Doğru" }
45
- ]
46
- }
@@ -1,18 +0,0 @@
1
- import { randomBytes } from './crypto';
2
- import enData from '../locale/en.json';
3
- import trData from '../locale/tr.json';
4
-
5
- export class LogicGenerator {
6
- private static data: Record<'en' | 'tr', { question: string; answer: string }[]> = {
7
- en: enData.logics,
8
- tr: trData.logics
9
- };
10
-
11
- static getRandom(locale: 'en' | 'tr' = 'en', difficulty: 'easy' | 'medium' | 'hard' = 'easy'): { question: string; answer: string } {
12
- const logics = this.data[locale];
13
- // use crypto random to select a logic puzzle securely
14
- const buffer = randomBytes(4);
15
- const index = buffer.readUInt32LE(0) % logics.length;
16
- return logics[index]!;
17
- }
18
- }
@@ -1,18 +0,0 @@
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
- }