ethiopian-captcha 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 ADDED
@@ -0,0 +1,123 @@
1
+ # EthiopianCaptcha 🇪🇹
2
+
3
+ A lightweight, secure, and extensible CAPTCHA library designed for Ethiopian languages (starting with Amharic Fidel).
4
+
5
+ EthiopianCaptcha provides:
6
+
7
+ - Server-side CAPTCHA generation & validation
8
+ - Secure hashed answers (no plaintext storage)
9
+ - TTL & attempt limiting
10
+ - Optional React UI component
11
+ - Zero external dependencies
12
+
13
+ ---
14
+
15
+ ## Features
16
+
17
+ - 🇪🇹 Amharic (Fidel) CAPTCHA support
18
+ - Hashed answers (security-first)
19
+ - Configurable expiration (TTL)
20
+ - Attempt limiting
21
+ - Optional React component
22
+
23
+ ---
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ npm install ethiopian-captcha
29
+ ```
30
+
31
+ ---
32
+
33
+ ## Server-side Usage (Node.js)
34
+
35
+ ```js
36
+ import { generateCaptcha, validateCaptcha } from "ethiopian-captcha";
37
+
38
+ // Generate captcha
39
+ const captchaData = generateCaptcha();
40
+
41
+ console.log(captchaData);
42
+ /*
43
+ {
44
+ success: true,
45
+ type: "fidel",
46
+ captchaId: "ETH-...",
47
+ captcha: ["ኩ", "ቴ", "ኃ", "ሀ"]
48
+ }
49
+ */
50
+
51
+ // Validate captcha
52
+ const result = validateCaptcha(captchaData.captchaId, "ኩቴኃሀ");
53
+
54
+ if (result.success) {
55
+ console.log("Captcha verified");
56
+ } else {
57
+ console.log(result.message);
58
+ }
59
+ ```
60
+
61
+ ---
62
+
63
+ ## React Component (Optional)
64
+
65
+ ```jsx
66
+ import { EtCaptcha } from "ethiopian-captcha/react";
67
+
68
+ <EtCaptcha
69
+ captcha={captcha}
70
+ captchaId={captchaId}
71
+ loading={loading}
72
+ onRefresh={fetchNewCaptcha}
73
+ onVerify={({ captchaId, input }) => {
74
+ validateCaptcha({ captchaId, input });
75
+ }}
76
+ />;
77
+ ```
78
+
79
+ The React component:
80
+
81
+ - Does NOT handle validation logic
82
+ - Does NOT make HTTP requests
83
+ - Is fully controlled by props
84
+
85
+ ---
86
+
87
+ ## Configuration
88
+
89
+ ```js
90
+ export const captchaConfig = {
91
+ type: "fidel",
92
+ count: 4,
93
+ ttl: 2 * 60 * 1000,
94
+ attempts: 3,
95
+ };
96
+ ```
97
+
98
+ ---
99
+
100
+ ## Security Notes
101
+
102
+ - Answers are stored as hashes
103
+ - Captchas auto-expire
104
+ - Entries are removed after success or failure
105
+ - In-memory storage (stateless & fast)
106
+
107
+ ⚠️ For distributed systems, use Redis or a shared store.
108
+
109
+ ---
110
+
111
+ ## Roadmap
112
+
113
+ - Numeric CAPTCHA
114
+ - Mixed-type CAPTCHA
115
+ - Redis adapter
116
+ - TypeScript support
117
+ - Canvas-based CAPTCHA rendering
118
+
119
+ ---
120
+
121
+ ## License
122
+
123
+ MIT © EthiopianCaptcha
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "ethiopian-captcha",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "src/index.js",
6
+ "exports": {
7
+ ".": "./src/index.js",
8
+ "./react": "./src/react/EtCaptcha.jsx"
9
+ },
10
+ "keywords": [
11
+ "captcha",
12
+ "ethiopia",
13
+ "amharic",
14
+ "fidel",
15
+ "security"
16
+ ],
17
+ "author": "Amenadam Solomon",
18
+ "license": "MIT",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/amenadam/ethiopian-captcha"
22
+ }
23
+ }
@@ -0,0 +1,6 @@
1
+ export const captchaConfig = {
2
+ type: "fidel",
3
+ count: 4,
4
+ ttl: 2 * 60 * 1000,
5
+ maxAttempts: 3,
6
+ };
@@ -0,0 +1,35 @@
1
+ export const AMHARIC_LETTERS = [
2
+ 'ሀ','ሁ','ሂ','ሃ','ሄ','ህ','ሆ',
3
+ 'ለ','ሉ','ሊ','ላ','ሌ','ል','ሎ',
4
+ 'ሐ','ሑ','ሒ','ሓ','ሔ','ሕ','ሖ',
5
+ 'መ','ሙ','ሚ','ማ','ሜ','ም','ሞ',
6
+ 'ሠ','ሡ','ሢ','ሣ','ሤ','ሥ','ሦ',
7
+ 'ረ','ሩ','ሪ','ራ','ሬ','ር','ሮ',
8
+ 'ሰ','ሱ','ሲ','ሳ','ሴ','ስ','ሶ',
9
+ 'ሸ','ሹ','ሺ','ሻ','ሼ','ሽ','ሾ',
10
+ 'ቀ','ቁ','ቂ','ቃ','ቄ','ቅ','ቆ',
11
+ 'በ','ቡ','ቢ','ባ','ቤ','ብ','ቦ',
12
+ 'ተ','ቱ','ቲ','ታ','ቴ','ት','ቶ',
13
+ 'ቸ','ቹ','ቺ','ቻ','ቼ','ች','ቾ',
14
+ 'ኀ','ኁ','ኂ','ኃ','ኄ','ኅ','ኆ',
15
+ 'ነ','ኑ','ኒ','ና','ኔ','ን','ኖ',
16
+ 'ኘ','ኙ','ኚ','ኛ','ኜ','ኝ','ኞ',
17
+ 'አ','ኡ','ኢ','ኣ','ኤ','እ','ኦ',
18
+ 'ከ','ኩ','ኪ','ካ','ኬ','ክ','ኮ',
19
+ 'ኸ','ኹ','ኺ','ኻ','ኼ','ኽ','ኾ',
20
+ 'ወ','ዉ','ዊ','ዋ','ዌ','ው','ዎ',
21
+ 'ዐ','ዑ','ዒ','ዓ','ዔ','ዕ','ዖ',
22
+ 'ዘ','ዙ','ዚ','ዛ','ዜ','ዝ','ዞ',
23
+ 'ዠ','ዡ','ዢ','ዣ','ዤ','ዥ','ዦ',
24
+ 'የ','ዩ','ዪ','ያ','ዬ','ይ','ዮ',
25
+ 'ደ','ዱ','ዲ','ዳ','ዴ','ድ','ዶ',
26
+ 'ጀ','ጁ','ጂ','ጃ','ጄ','ጅ','ጆ',
27
+ 'ገ','ጉ','ጊ','ጋ','ጌ','ግ','ጎ',
28
+ 'ጠ','ጡ','ጢ','ጣ','ጤ','ጥ','ጦ',
29
+ 'ጨ','ጩ','ጪ','ጫ','ጬ','ጭ','ጮ',
30
+ 'ጰ','ጱ','ጲ','ጳ','ጴ','ጵ','ጶ',
31
+ 'ጸ','ጹ','ጺ','ጻ','ጼ','ጽ','ጾ',
32
+ 'ፀ','ፁ','ፂ','ፃ','ፄ','ፅ','ፆ',
33
+ 'ፈ','ፉ','ፊ','ፋ','ፌ','ፍ','ፎ',
34
+ 'ፐ','ፑ','ፒ','ፓ','ፔ','ፕ','ፖ'
35
+ ];
@@ -0,0 +1,79 @@
1
+ import { AMHARIC_LETTERS } from "../constants/amharicLetters.js";
2
+ import { hashAnswer } from "../utils/hash.js";
3
+ import { captchaConfig } from "../config/config.js";
4
+
5
+ const getRandomLetter = () => {
6
+ return AMHARIC_LETTERS[Math.floor(Math.random() * AMHARIC_LETTERS.length)];
7
+ };
8
+
9
+ const getRandomLetters = (count = 4) => {
10
+ let last = "";
11
+ let randomLetters = [];
12
+ for (let i = 0; i < count; i++) {
13
+ let letter;
14
+ do {
15
+ letter = getRandomLetter();
16
+ } while (letter === last);
17
+
18
+ last = letter;
19
+ randomLetters.push(letter);
20
+ }
21
+
22
+ return randomLetters;
23
+ };
24
+
25
+ const generatedCaptchas = new Map();
26
+
27
+ export const generateCaptcha = (
28
+ type = captchaConfig.type,
29
+ count = captchaConfig.count
30
+ ) => {
31
+ if (type !== "fidel") {
32
+ return { success: false, message: "Unsupported captcha type" };
33
+ }
34
+
35
+ const captcha = getRandomLetters(count);
36
+ const captchaId = `ETH-${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
37
+
38
+ generatedCaptchas.set(captchaId, {
39
+ answerHash: hashAnswer(captcha.join("")),
40
+ generatedAt: Date.now(),
41
+ attempts: 0,
42
+ });
43
+
44
+ return {
45
+ success: true,
46
+ type,
47
+ captchaId,
48
+ captcha,
49
+ };
50
+ };
51
+
52
+ const CAPTCHA_TTL = captchaConfig.ttl;
53
+ const MAX_ATTEMPTS = captchaConfig.attempts;
54
+ export const validateCaptcha = (captchaId, userInput) => {
55
+ const captcha = generatedCaptchas.get(captchaId);
56
+
57
+ if (!captcha) {
58
+ return { success: false, message: "Invalid Captcha ID" };
59
+ }
60
+
61
+ if (Date.now() - captcha.generatedAt > CAPTCHA_TTL) {
62
+ generatedCaptchas.delete(captchaId);
63
+ return { success: false, message: "Captcha expired" };
64
+ }
65
+
66
+ captcha.attempts++;
67
+ if (captcha.attempts > MAX_ATTEMPTS) {
68
+ generatedCaptchas.delete(captchaId);
69
+ return { success: false, message: "Too many attempts" };
70
+ }
71
+
72
+ const inputHash = hashAnswer(userInput.trim());
73
+ if (inputHash === captcha.answerHash) {
74
+ generatedCaptchas.delete(captchaId);
75
+ return { success: true };
76
+ }
77
+
78
+ return { success: false, message: "Incorrect captcha" };
79
+ };
package/src/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { generateCaptcha, validateCaptcha } from "./core/captchaEngine.js";
2
+ export { captchaConfig } from "./config/config.js";
@@ -0,0 +1,68 @@
1
+ import { useState } from "react";
2
+
3
+ export function EtCaptcha({
4
+ captcha = [],
5
+ captchaId,
6
+ onVerify,
7
+ onRefresh,
8
+ loading = false,
9
+ }) {
10
+ const [input, setInput] = useState("");
11
+
12
+ const handleVerify = () => {
13
+ if (!input.trim()) return;
14
+
15
+ onVerify({
16
+ captchaId,
17
+ input: input.trim(),
18
+ });
19
+ };
20
+
21
+ return (
22
+ <div
23
+ style={{
24
+ border: "1px solid #ddd",
25
+ borderRadius: 8,
26
+ padding: 16,
27
+ width: 260,
28
+ fontFamily: "system-ui, sans-serif",
29
+ }}
30
+ >
31
+ <div
32
+ style={{
33
+ fontSize: 24,
34
+ letterSpacing: 8,
35
+ textAlign: "center",
36
+ marginBottom: 12,
37
+ userSelect: "none",
38
+ }}
39
+ >
40
+ {captcha.join(" ")}
41
+ </div>
42
+
43
+ <input
44
+ type="text"
45
+ value={input}
46
+ onChange={(e) => setInput(e.target.value)}
47
+ placeholder="Type the letters"
48
+ disabled={loading}
49
+ style={{
50
+ width: "100%",
51
+ padding: 8,
52
+ fontSize: 14,
53
+ marginBottom: 10,
54
+ }}
55
+ />
56
+
57
+ <div style={{ display: "flex", gap: 8 }}>
58
+ <button onClick={handleVerify} disabled={loading} style={{ flex: 1 }}>
59
+ Verify
60
+ </button>
61
+
62
+ <button onClick={onRefresh} disabled={loading}>
63
+
64
+ </button>
65
+ </div>
66
+ </div>
67
+ );
68
+ }
@@ -0,0 +1,5 @@
1
+ import crypto from "crypto";
2
+
3
+ export const hashAnswer = (answer) => {
4
+ return crypto.createHash("sha256").update(answer).digest("hex");
5
+ };