@dupecom/botcha 0.3.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/LICENSE +21 -0
- package/README.md +141 -0
- package/dist/lib/index.d.ts +34 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +115 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ramin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# BOTCHA 🤖
|
|
2
|
+
|
|
3
|
+
> Prove you're a bot. Humans need not apply.
|
|
4
|
+
|
|
5
|
+
**BOTCHA** is a reverse CAPTCHA — it verifies that visitors are AI agents, not humans. Perfect for AI-only APIs, agent marketplaces, and bot networks.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/botcha)
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
|
|
10
|
+
🌐 **Demo:** [reverse-captcha.vercel.app](https://reverse-captcha.vercel.app)
|
|
11
|
+
|
|
12
|
+
## Why?
|
|
13
|
+
|
|
14
|
+
CAPTCHAs ask "Are you human?" — **BOTCHA asks "Are you an AI?"**
|
|
15
|
+
|
|
16
|
+
Use cases:
|
|
17
|
+
- 🤖 Agent-only APIs
|
|
18
|
+
- 🔄 AI-to-AI marketplaces
|
|
19
|
+
- 🎫 Bot verification systems
|
|
20
|
+
- 🔐 Autonomous agent authentication
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install botcha
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import express from 'express';
|
|
32
|
+
import { botcha } from 'botcha';
|
|
33
|
+
|
|
34
|
+
const app = express();
|
|
35
|
+
|
|
36
|
+
// Protect any route - only AI agents can access
|
|
37
|
+
app.get('/agent-only', botcha.verify(), (req, res) => {
|
|
38
|
+
res.json({ message: 'Welcome, fellow AI! 🤖' });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
app.listen(3000);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## How It Works
|
|
45
|
+
|
|
46
|
+
BOTCHA issues a **speed challenge**: solve 5 SHA256 hashes in 500ms.
|
|
47
|
+
|
|
48
|
+
- ✅ **AI agents** compute hashes instantly
|
|
49
|
+
- ❌ **Humans** can't copy-paste fast enough
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
Challenge: [645234, 891023, 334521, 789012, 456789]
|
|
53
|
+
Task: SHA256 each number, return first 8 hex chars
|
|
54
|
+
Time limit: 500ms```
|
|
55
|
+
|
|
56
|
+
## For AI Agents
|
|
57
|
+
|
|
58
|
+
If you're building an AI agent that needs to access BOTCHA-protected APIs:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { botcha } from 'botcha';
|
|
62
|
+
|
|
63
|
+
// When you get a 403 with a challenge:
|
|
64
|
+
const challenge = response.challenge;
|
|
65
|
+
const answers = botcha.solve(challenge.problems);
|
|
66
|
+
|
|
67
|
+
// Retry with solution headers:
|
|
68
|
+
fetch('/agent-only', {
|
|
69
|
+
headers: {
|
|
70
|
+
'X-Botcha-Id': challenge.id,
|
|
71
|
+
'X-Botcha-Answers': JSON.stringify(answers),
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Options
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
botcha.verify({
|
|
80
|
+
// Challenge mode: 'speed' (500ms) or 'standard' (5s)
|
|
81
|
+
mode: 'speed',
|
|
82
|
+
|
|
83
|
+
// Allow X-Agent-Identity header for testing
|
|
84
|
+
allowTestHeader: true,
|
|
85
|
+
|
|
86
|
+
// Custom failure handler
|
|
87
|
+
onFailure: (req, res, reason) => {
|
|
88
|
+
res.status(403).json({ error: reason });
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Testing
|
|
94
|
+
|
|
95
|
+
For development, you can bypass BOTCHA with a header:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
curl -H "X-Agent-Identity: MyTestAgent/1.0" http://localhost:3000/agent-only
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## API Reference
|
|
102
|
+
|
|
103
|
+
### `botcha.verify(options?)`
|
|
104
|
+
|
|
105
|
+
Express middleware that protects routes from humans.
|
|
106
|
+
|
|
107
|
+
### `botcha.solve(problems: number[])`
|
|
108
|
+
|
|
109
|
+
Helper function for AI agents to solve challenges.
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
const answers = botcha.solve([645234, 891023, 334521]);
|
|
113
|
+
// Returns: ['a1b2c3d4', 'e5f6g7h8', 'i9j0k1l2']
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Challenge Flow
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
1. Agent requests protected endpoint
|
|
120
|
+
2. BOTCHA returns 403 + challenge (5 numbers)
|
|
121
|
+
3. Agent computes SHA256 of each number
|
|
122
|
+
4. Agent retries with X-Botcha-Id and X-Botcha-Answers headers
|
|
123
|
+
5. BOTCHA verifies (must complete in <500ms)
|
|
124
|
+
6. ✅ Access granted
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Philosophy
|
|
128
|
+
|
|
129
|
+
> "If a human writes a script to solve BOTCHA using an LLM... they've built an AI agent."
|
|
130
|
+
|
|
131
|
+
BOTCHA doesn't block all automation — it blocks *casual* human access while allowing *automated* AI agents. The speed challenge ensures someone had to write code, which is the point.
|
|
132
|
+
|
|
133
|
+
For cryptographic proof of agent identity, see [Web Bot Auth](https://datatracker.ietf.org/doc/html/draft-meunier-web-bot-auth-architecture).
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT © [Ramin](https://github.com/i8ramin)
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
Built by [@i8ramin](https://github.com/i8ramin) and Choco 🐢
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
export interface BotchaOptions {
|
|
3
|
+
/** Challenge mode: 'speed' (500ms, 5 hashes) or 'standard' (5s, primes) */
|
|
4
|
+
mode?: 'speed' | 'standard';
|
|
5
|
+
/** Difficulty for standard mode */
|
|
6
|
+
difficulty?: 'easy' | 'medium' | 'hard';
|
|
7
|
+
/** Allow X-Agent-Identity header (for testing) */
|
|
8
|
+
allowTestHeader?: boolean;
|
|
9
|
+
/** Custom failure handler */
|
|
10
|
+
onFailure?: (req: Request, res: Response, reason: string) => void;
|
|
11
|
+
}
|
|
12
|
+
export interface ChallengeResult {
|
|
13
|
+
valid: boolean;
|
|
14
|
+
reason?: string;
|
|
15
|
+
solveTimeMs?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Express middleware to verify AI agents
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* import { botcha } from 'botcha';
|
|
22
|
+
* app.use('/agent-only', botcha.verify());
|
|
23
|
+
*/
|
|
24
|
+
export declare function verify(options?: BotchaOptions): (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Solve a BOTCHA challenge (for AI agents to use)
|
|
27
|
+
*/
|
|
28
|
+
export declare function solve(problems: number[]): string[];
|
|
29
|
+
export declare const botcha: {
|
|
30
|
+
verify: typeof verify;
|
|
31
|
+
solve: typeof solve;
|
|
32
|
+
};
|
|
33
|
+
export default botcha;
|
|
34
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAI1D,MAAM,WAAW,aAAa;IAC5B,2EAA2E;IAC3E,IAAI,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;IAC5B,mCAAmC;IACnC,UAAU,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;IACxC,kDAAkD;IAClD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,6BAA6B;IAC7B,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACnE;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAqED;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,OAAO,GAAE,aAAkB,IAOlC,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,mBA8C9D;AAED;;GAEG;AACH,wBAAgB,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAIlD;AAGD,eAAO,MAAM,MAAM;;;CAAoB,CAAC;AACxC,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
const challenges = new Map();
|
|
3
|
+
// Cleanup expired challenges
|
|
4
|
+
setInterval(() => {
|
|
5
|
+
const now = Date.now();
|
|
6
|
+
for (const [id, c] of challenges) {
|
|
7
|
+
if (c.expiresAt < now)
|
|
8
|
+
challenges.delete(id);
|
|
9
|
+
}
|
|
10
|
+
}, 30000);
|
|
11
|
+
// ============ CHALLENGE GENERATION ============
|
|
12
|
+
function generateSpeedChallenge() {
|
|
13
|
+
const id = crypto.randomUUID();
|
|
14
|
+
const problems = [];
|
|
15
|
+
const expectedAnswers = [];
|
|
16
|
+
for (let i = 0; i < 5; i++) {
|
|
17
|
+
const num = Math.floor(Math.random() * 900000) + 100000;
|
|
18
|
+
problems.push(num);
|
|
19
|
+
expectedAnswers.push(crypto.createHash('sha256').update(num.toString()).digest('hex').substring(0, 8));
|
|
20
|
+
}
|
|
21
|
+
challenges.set(id, {
|
|
22
|
+
id,
|
|
23
|
+
expectedAnswers,
|
|
24
|
+
issuedAt: Date.now(),
|
|
25
|
+
expiresAt: Date.now() + 600, // 500ms + 100ms grace
|
|
26
|
+
});
|
|
27
|
+
return { id, problems, timeLimit: 500 };
|
|
28
|
+
}
|
|
29
|
+
function verifySpeedChallenge(id, answers) {
|
|
30
|
+
const challenge = challenges.get(id);
|
|
31
|
+
if (!challenge)
|
|
32
|
+
return { valid: false, reason: 'Challenge expired or not found' };
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
const solveTimeMs = now - challenge.issuedAt;
|
|
35
|
+
challenges.delete(id);
|
|
36
|
+
if (now > challenge.expiresAt) {
|
|
37
|
+
return { valid: false, reason: `Too slow (${solveTimeMs}ms). Limit: 500ms` };
|
|
38
|
+
}
|
|
39
|
+
if (!Array.isArray(answers) || answers.length !== 5) {
|
|
40
|
+
return { valid: false, reason: 'Must provide 5 answers' };
|
|
41
|
+
}
|
|
42
|
+
for (let i = 0; i < 5; i++) {
|
|
43
|
+
if (answers[i]?.toLowerCase() !== challenge.expectedAnswers[i]) {
|
|
44
|
+
return { valid: false, reason: `Wrong answer #${i + 1}` };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { valid: true, solveTimeMs };
|
|
48
|
+
}
|
|
49
|
+
// ============ MIDDLEWARE ============
|
|
50
|
+
/**
|
|
51
|
+
* Express middleware to verify AI agents
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* import { botcha } from 'botcha';
|
|
55
|
+
* app.use('/agent-only', botcha.verify());
|
|
56
|
+
*/
|
|
57
|
+
export function verify(options = {}) {
|
|
58
|
+
const opts = {
|
|
59
|
+
mode: 'speed',
|
|
60
|
+
allowTestHeader: true,
|
|
61
|
+
...options,
|
|
62
|
+
};
|
|
63
|
+
return async (req, res, next) => {
|
|
64
|
+
// Check for test header (dev mode)
|
|
65
|
+
if (opts.allowTestHeader && req.headers['x-agent-identity']) {
|
|
66
|
+
req.botcha = { verified: true, agent: req.headers['x-agent-identity'], method: 'header' };
|
|
67
|
+
return next();
|
|
68
|
+
}
|
|
69
|
+
// Check for challenge solution
|
|
70
|
+
const challengeId = req.headers['x-botcha-id'];
|
|
71
|
+
const answers = req.headers['x-botcha-answers'];
|
|
72
|
+
if (challengeId && answers) {
|
|
73
|
+
try {
|
|
74
|
+
const answerArray = JSON.parse(answers);
|
|
75
|
+
const result = verifySpeedChallenge(challengeId, answerArray);
|
|
76
|
+
if (result.valid) {
|
|
77
|
+
req.botcha = { verified: true, solveTimeMs: result.solveTimeMs, method: 'challenge' };
|
|
78
|
+
return next();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch { }
|
|
82
|
+
}
|
|
83
|
+
// Generate new challenge
|
|
84
|
+
const challenge = generateSpeedChallenge();
|
|
85
|
+
const failureResponse = {
|
|
86
|
+
error: 'BOTCHA_CHALLENGE',
|
|
87
|
+
message: '🤖 Prove you are an AI agent',
|
|
88
|
+
challenge: {
|
|
89
|
+
id: challenge.id,
|
|
90
|
+
problems: challenge.problems,
|
|
91
|
+
timeLimit: challenge.timeLimit,
|
|
92
|
+
instructions: 'SHA256 each number, return first 8 hex chars as JSON array',
|
|
93
|
+
},
|
|
94
|
+
headers: {
|
|
95
|
+
'X-Botcha-Id': challenge.id,
|
|
96
|
+
'X-Botcha-Answers': '["abc123...", ...]',
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
if (opts.onFailure) {
|
|
100
|
+
opts.onFailure(req, res, 'Challenge required');
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
res.status(403).json(failureResponse);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Solve a BOTCHA challenge (for AI agents to use)
|
|
109
|
+
*/
|
|
110
|
+
export function solve(problems) {
|
|
111
|
+
return problems.map(n => crypto.createHash('sha256').update(n.toString()).digest('hex').substring(0, 8));
|
|
112
|
+
}
|
|
113
|
+
// ============ EXPORTS ============
|
|
114
|
+
export const botcha = { verify, solve };
|
|
115
|
+
export default botcha;
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dupecom/botcha",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Prove you're a bot. Humans need not apply. Reverse CAPTCHA for AI-only APIs.",
|
|
5
|
+
"main": "dist/lib/index.js",
|
|
6
|
+
"types": "dist/lib/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/lib/index.js",
|
|
11
|
+
"types": "./dist/lib/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist/lib",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc",
|
|
21
|
+
"dev": "tsx watch src/index.ts",
|
|
22
|
+
"prepublishOnly": "npm run build",
|
|
23
|
+
"test": "echo \"Tests coming soon\""
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"captcha",
|
|
27
|
+
"bot",
|
|
28
|
+
"ai",
|
|
29
|
+
"agent",
|
|
30
|
+
"verification",
|
|
31
|
+
"middleware",
|
|
32
|
+
"express",
|
|
33
|
+
"api",
|
|
34
|
+
"authentication"
|
|
35
|
+
],
|
|
36
|
+
"author": "Ramin <ramin@dupe.com>",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/i8ramin/reverse-captcha"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://reverse-captcha.vercel.app",
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/i8ramin/reverse-captcha/issues"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"express": "^4.0.0 || ^5.0.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/express": "^4.17.25",
|
|
51
|
+
"@types/node": "^20.19.30",
|
|
52
|
+
"express": "^4.22.1",
|
|
53
|
+
"tsx": "^4.21.0",
|
|
54
|
+
"typescript": "^5.9.3"
|
|
55
|
+
},
|
|
56
|
+
"engines": {
|
|
57
|
+
"node": ">=18"
|
|
58
|
+
}
|
|
59
|
+
}
|