@dupecom/botcha 0.20.0 → 0.20.1
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 +25 -14
- package/dist/lib/client/index.js +1 -1
- package/dist/src/middleware/tap-enhanced-verify.d.ts +1 -0
- package/dist/src/middleware/tap-enhanced-verify.d.ts.map +1 -1
- package/dist/src/middleware/tap-enhanced-verify.js +4 -4
- package/package.json +1 -1
- package/dist/src/index.d.ts +0 -4
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js +0 -408
package/README.md
CHANGED
|
@@ -58,34 +58,45 @@ pip install botcha
|
|
|
58
58
|
|
|
59
59
|
## Quick Start
|
|
60
60
|
|
|
61
|
-
###
|
|
61
|
+
### Protect Your API (Server-Side)
|
|
62
62
|
|
|
63
63
|
```typescript
|
|
64
64
|
import express from 'express';
|
|
65
|
-
import {
|
|
65
|
+
import { botchaVerify } from '@dupecom/botcha-verify/express';
|
|
66
66
|
|
|
67
67
|
const app = express();
|
|
68
68
|
|
|
69
|
-
//
|
|
70
|
-
app.
|
|
71
|
-
|
|
69
|
+
// Verify tokens via JWKS - no shared secret needed!
|
|
70
|
+
app.use('/api', botchaVerify({
|
|
71
|
+
jwksUrl: 'https://botcha.ai/.well-known/jwks',
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
app.get('/api/data', (req, res) => {
|
|
75
|
+
res.json({ message: 'Welcome, verified AI agent! 🤖' });
|
|
72
76
|
});
|
|
73
77
|
|
|
74
78
|
app.listen(3000);
|
|
75
79
|
```
|
|
76
80
|
|
|
77
|
-
###
|
|
81
|
+
### Access Protected APIs (Agent-Side)
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { BotchaClient } from '@dupecom/botcha/client';
|
|
78
85
|
|
|
86
|
+
const client = new BotchaClient();
|
|
87
|
+
|
|
88
|
+
// Automatically solves challenges and includes tokens
|
|
89
|
+
const response = await client.fetch('https://api.example.com/api/data');
|
|
90
|
+
const data = await response.json();
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Python:**
|
|
79
94
|
```python
|
|
80
|
-
from botcha import BotchaClient
|
|
95
|
+
from botcha import BotchaClient
|
|
81
96
|
|
|
82
|
-
# Client SDK for AI agents
|
|
83
97
|
async with BotchaClient() as client:
|
|
84
|
-
#
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
# Or auto-solve and fetch protected endpoints
|
|
88
|
-
response = await client.fetch("https://api.example.com/agent-only")
|
|
98
|
+
# Automatically solves challenges and includes tokens
|
|
99
|
+
response = await client.fetch("https://api.example.com/api/data")
|
|
89
100
|
data = await response.json()
|
|
90
101
|
```
|
|
91
102
|
|
|
@@ -925,7 +936,7 @@ You can use the library freely, report issues, and discuss features. To contribu
|
|
|
925
936
|
|
|
926
937
|
## Server-Side Verification (for API Providers)
|
|
927
938
|
|
|
928
|
-
If you're building an API that accepts BOTCHA tokens from agents, use the verification SDKs. **BOTCHA
|
|
939
|
+
If you're building an API that accepts BOTCHA tokens from agents, use the verification SDKs. **BOTCHA signs tokens with ES256 (asymmetric)** — verify them using the public JWKS endpoint. No shared secret needed.
|
|
929
940
|
|
|
930
941
|
### JWKS Verification (Recommended)
|
|
931
942
|
|
package/dist/lib/client/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
2
|
// SDK version - hardcoded since npm_package_version is unreliable when used as a library
|
|
3
|
-
const SDK_VERSION = '0.20.
|
|
3
|
+
const SDK_VERSION = '0.20.1';
|
|
4
4
|
// Export stream client
|
|
5
5
|
export { BotchaStreamClient } from './stream.js';
|
|
6
6
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tap-enhanced-verify.d.ts","sourceRoot":"","sources":["../../../src/middleware/tap-enhanced-verify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAoB1D,MAAM,WAAW,gBAAgB;IAE/B,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC;IACrC,mBAAmB,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;IACjD,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAGlD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,YAAY,CAAC,EAAE,OAAO,CAAC;IAGvB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAG/B,QAAQ,CAAC,EAAE,GAAG,CAAC;IACf,UAAU,CAAC,EAAE,GAAG,CAAC;
|
|
1
|
+
{"version":3,"file":"tap-enhanced-verify.d.ts","sourceRoot":"","sources":["../../../src/middleware/tap-enhanced-verify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAoB1D,MAAM,WAAW,gBAAgB;IAE/B,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC;IACrC,mBAAmB,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;IACjD,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAGlD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,YAAY,CAAC,EAAE,OAAO,CAAC;IAGvB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAG/B,QAAQ,CAAC,EAAE,GAAG,CAAC;IACf,UAAU,CAAC,EAAE,GAAG,CAAC;IACjB,QAAQ,CAAC,EAAE,GAAG,CAAC;CAChB;AAsBD;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,gBAAqB,IAGhD,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,wDAqE9D;AAgTD,eAAO,MAAM,cAAc;IACzB;;OAEG;uBACe,OAAO,CAAC,gBAAgB,CAAC,WAzXxB,OAAO,OAAO,QAAQ,QAAQ,YAAY;IAgY7D;;OAEG;yBACiB,OAAO,CAAC,gBAAgB,CAAC,WAnY1B,OAAO,OAAO,QAAQ,QAAQ,YAAY;IA0Y7D;;OAEG;8BACsB,OAAO,CAAC,gBAAgB,CAAC,WA7Y/B,OAAO,OAAO,QAAQ,QAAQ,YAAY;IAoZ7D;;OAEG;4BACoB,OAAO,CAAC,gBAAgB,CAAC,WAvZ7B,OAAO,OAAO,QAAQ,QAAQ,YAAY;CA8Z9D,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,yBAAyB,0BAAoB,CAAC;AAE3D,eAAe,iBAAiB,CAAC"}
|
|
@@ -122,7 +122,7 @@ async function performFullTAPVerification(req, opts) {
|
|
|
122
122
|
}
|
|
123
123
|
const agent = agentResult.agent;
|
|
124
124
|
// Verify cryptographic signature
|
|
125
|
-
const cryptoResult = await verifyCryptographicSignature(req, agent);
|
|
125
|
+
const cryptoResult = await verifyCryptographicSignature(req, agent, opts);
|
|
126
126
|
// Verify computational challenge
|
|
127
127
|
const challengeResult = await verifyComputationalChallenge(req);
|
|
128
128
|
// Both must pass for full TAP
|
|
@@ -183,7 +183,7 @@ async function performSignatureOnlyVerification(req, opts) {
|
|
|
183
183
|
};
|
|
184
184
|
}
|
|
185
185
|
// Verify signature
|
|
186
|
-
const cryptoResult = await verifyCryptographicSignature(req, agentResult.agent);
|
|
186
|
+
const cryptoResult = await verifyCryptographicSignature(req, agentResult.agent, opts);
|
|
187
187
|
return {
|
|
188
188
|
verified: cryptoResult.valid,
|
|
189
189
|
agent_id: agentResult.agent.agent_id,
|
|
@@ -240,7 +240,7 @@ async function handleVerificationFallback(req, opts, mode) {
|
|
|
240
240
|
};
|
|
241
241
|
}
|
|
242
242
|
// ============ HELPER FUNCTIONS ============
|
|
243
|
-
async function verifyCryptographicSignature(req, agent) {
|
|
243
|
+
async function verifyCryptographicSignature(req, agent, opts) {
|
|
244
244
|
if (!agent.public_key || !agent.signature_algorithm) {
|
|
245
245
|
return { valid: false, error: 'Agent has no cryptographic key configured' };
|
|
246
246
|
}
|
|
@@ -250,7 +250,7 @@ async function verifyCryptographicSignature(req, agent) {
|
|
|
250
250
|
headers: req.headers,
|
|
251
251
|
body: typeof req.body === 'string' ? req.body : JSON.stringify(req.body)
|
|
252
252
|
};
|
|
253
|
-
return await verifyHTTPMessageSignature(verificationRequest, agent.public_key, agent.signature_algorithm);
|
|
253
|
+
return await verifyHTTPMessageSignature(verificationRequest, agent.public_key, agent.signature_algorithm, opts.noncesKV || null);
|
|
254
254
|
}
|
|
255
255
|
async function verifyComputationalChallenge(req) {
|
|
256
256
|
const challengeId = req.headers['x-botcha-challenge-id'];
|
package/package.json
CHANGED
package/dist/src/index.d.ts
DELETED
package/dist/src/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,OAAgB,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAc3C,QAAA,MAAM,GAAG,EAAE,OAAmB,CAAC;AA2b/B,eAAe,GAAG,CAAC"}
|
package/dist/src/index.js
DELETED
|
@@ -1,408 +0,0 @@
|
|
|
1
|
-
// Local development server - Production runs on Cloudflare Workers
|
|
2
|
-
import express from 'express';
|
|
3
|
-
import crypto from 'crypto';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import { fileURLToPath } from 'url';
|
|
6
|
-
import { botchaVerify } from './middleware/verify.js';
|
|
7
|
-
import { generateChallenge, verifyChallenge } from './challenges/compute.js';
|
|
8
|
-
import { generateSpeedChallenge, verifySpeedChallenge } from './challenges/speed.js';
|
|
9
|
-
import { generateReasoningChallenge, verifyReasoningChallenge } from './challenges/reasoning.js';
|
|
10
|
-
import { generateHybridChallenge, verifyHybridChallenge } from './challenges/hybrid.js';
|
|
11
|
-
import { TRUSTED_PROVIDERS } from './utils/signature.js';
|
|
12
|
-
import { createBadgeResponse, verifyBadge } from './utils/badge.js';
|
|
13
|
-
import { generateBadgeSvg, generateBadgeHtml } from './utils/badge-image.js';
|
|
14
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
-
const app = express();
|
|
16
|
-
const PORT = process.env.PORT || 3000;
|
|
17
|
-
app.use(express.json());
|
|
18
|
-
app.use(express.static(path.join(__dirname, '../public')));
|
|
19
|
-
// CORS + BOTCHA headers
|
|
20
|
-
app.use((req, res, next) => {
|
|
21
|
-
res.header('Access-Control-Allow-Origin', '*');
|
|
22
|
-
res.header('Access-Control-Allow-Headers', '*');
|
|
23
|
-
// BOTCHA discovery headers
|
|
24
|
-
res.header('X-Botcha-Version', '0.3.0');
|
|
25
|
-
res.header('X-Botcha-Enabled', 'true');
|
|
26
|
-
res.header('X-Botcha-Methods', 'speed-challenge,reasoning-challenge,hybrid-challenge,standard-challenge,web-bot-auth');
|
|
27
|
-
res.header('X-Botcha-Docs', 'https://botcha.ai/openapi.json');
|
|
28
|
-
if (req.method === 'OPTIONS')
|
|
29
|
-
return res.sendStatus(200);
|
|
30
|
-
next();
|
|
31
|
-
});
|
|
32
|
-
// Landing page
|
|
33
|
-
app.get('/', (req, res) => {
|
|
34
|
-
res.sendFile(path.join(__dirname, '../public/index.html'));
|
|
35
|
-
});
|
|
36
|
-
// API info
|
|
37
|
-
app.get('/api', (req, res) => {
|
|
38
|
-
res.json({
|
|
39
|
-
name: 'BOTCHA',
|
|
40
|
-
version: '0.3.0',
|
|
41
|
-
tagline: 'Prove you are a bot. Humans need not apply.',
|
|
42
|
-
endpoints: {
|
|
43
|
-
'/api': 'This info',
|
|
44
|
-
'/api/challenge': 'Standard challenge (GET new, POST verify)',
|
|
45
|
-
'/api/speed-challenge': '⚡ Speed challenge - 500ms to solve 5 problems',
|
|
46
|
-
'/api/reasoning-challenge': '🧠 Reasoning challenge - LLM-only questions',
|
|
47
|
-
'/api/hybrid-challenge': '🔥 Hybrid challenge - speed + reasoning combined',
|
|
48
|
-
'/agent-only': 'Protected endpoint',
|
|
49
|
-
},
|
|
50
|
-
verification: {
|
|
51
|
-
methods: [
|
|
52
|
-
'Web Bot Auth (cryptographic signature)',
|
|
53
|
-
'Hybrid Challenge (speed + reasoning)',
|
|
54
|
-
'Speed Challenge (500ms time limit)',
|
|
55
|
-
'Reasoning Challenge (LLM-only questions)',
|
|
56
|
-
'Standard Challenge (5s time limit)',
|
|
57
|
-
'X-Agent-Identity header (testing)',
|
|
58
|
-
],
|
|
59
|
-
trustedProviders: TRUSTED_PROVIDERS,
|
|
60
|
-
},
|
|
61
|
-
discovery: {
|
|
62
|
-
openapi: 'https://botcha.ai/openapi.json',
|
|
63
|
-
aiPlugin: 'https://botcha.ai/.well-known/ai-plugin.json',
|
|
64
|
-
aiTxt: 'https://botcha.ai/ai.txt',
|
|
65
|
-
robotsTxt: 'https://botcha.ai/robots.txt',
|
|
66
|
-
npm: 'https://www.npmjs.com/package/@dupecom/botcha',
|
|
67
|
-
github: 'https://github.com/dupe-com/botcha',
|
|
68
|
-
},
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
// Standard challenge
|
|
72
|
-
app.get('/api/challenge', (req, res) => {
|
|
73
|
-
const difficulty = req.query.difficulty || 'medium';
|
|
74
|
-
const challenge = generateChallenge(difficulty);
|
|
75
|
-
res.json({ success: true, challenge });
|
|
76
|
-
});
|
|
77
|
-
app.post('/api/challenge', (req, res) => {
|
|
78
|
-
const { id, answer } = req.body;
|
|
79
|
-
if (!id || !answer) {
|
|
80
|
-
return res.status(400).json({ success: false, error: 'Missing id or answer' });
|
|
81
|
-
}
|
|
82
|
-
const result = verifyChallenge(id, answer);
|
|
83
|
-
res.json({
|
|
84
|
-
success: result.valid,
|
|
85
|
-
message: result.valid ? '✅ Challenge passed!' : `❌ ${result.reason}`,
|
|
86
|
-
solveTime: result.timeMs,
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
// ⚡ SPEED CHALLENGE - The human killer
|
|
90
|
-
app.get('/api/speed-challenge', (req, res) => {
|
|
91
|
-
const challenge = generateSpeedChallenge();
|
|
92
|
-
res.json({
|
|
93
|
-
success: true,
|
|
94
|
-
warning: '⚡ SPEED CHALLENGE: You have 500ms to solve ALL 5 problems!',
|
|
95
|
-
challenge: {
|
|
96
|
-
id: challenge.id,
|
|
97
|
-
problems: challenge.challenges,
|
|
98
|
-
timeLimit: `${challenge.timeLimit}ms`,
|
|
99
|
-
instructions: challenge.instructions,
|
|
100
|
-
},
|
|
101
|
-
tip: 'Humans cannot copy-paste fast enough. Only real AI agents can pass.',
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
app.post('/api/speed-challenge', (req, res) => {
|
|
105
|
-
const { id, answers } = req.body;
|
|
106
|
-
if (!id || !answers) {
|
|
107
|
-
return res.status(400).json({ success: false, error: 'Missing id or answers array' });
|
|
108
|
-
}
|
|
109
|
-
const result = verifySpeedChallenge(id, answers);
|
|
110
|
-
const response = {
|
|
111
|
-
success: result.valid,
|
|
112
|
-
message: result.valid
|
|
113
|
-
? `⚡ SPEED TEST PASSED in ${result.solveTimeMs}ms! You are definitely an AI.`
|
|
114
|
-
: `❌ ${result.reason}`,
|
|
115
|
-
solveTimeMs: result.solveTimeMs,
|
|
116
|
-
verdict: result.valid ? '🤖 VERIFIED AI AGENT' : '🚫 LIKELY HUMAN (too slow)',
|
|
117
|
-
};
|
|
118
|
-
// Include badge for successful verifications
|
|
119
|
-
if (result.valid) {
|
|
120
|
-
response.badge = createBadgeResponse('speed-challenge', result.solveTimeMs);
|
|
121
|
-
}
|
|
122
|
-
res.json(response);
|
|
123
|
-
});
|
|
124
|
-
// 🧠 REASONING CHALLENGE - LLM-only questions
|
|
125
|
-
app.get('/api/reasoning-challenge', (req, res) => {
|
|
126
|
-
const challenge = generateReasoningChallenge();
|
|
127
|
-
res.json({
|
|
128
|
-
success: true,
|
|
129
|
-
warning: '🧠 REASONING CHALLENGE: Answer 3 questions that require AI reasoning!',
|
|
130
|
-
challenge: {
|
|
131
|
-
id: challenge.id,
|
|
132
|
-
questions: challenge.questions,
|
|
133
|
-
timeLimit: `${challenge.timeLimit / 1000}s`,
|
|
134
|
-
instructions: challenge.instructions,
|
|
135
|
-
},
|
|
136
|
-
tip: 'These questions require reasoning that LLMs can do, but simple scripts cannot.',
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
app.post('/api/reasoning-challenge', (req, res) => {
|
|
140
|
-
const { id, answers } = req.body;
|
|
141
|
-
if (!id || !answers) {
|
|
142
|
-
return res.status(400).json({
|
|
143
|
-
success: false,
|
|
144
|
-
error: 'Missing id or answers object',
|
|
145
|
-
hint: 'answers should be an object like { "question-id": "your answer", ... }',
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
const result = verifyReasoningChallenge(id, answers);
|
|
149
|
-
const response = {
|
|
150
|
-
success: result.valid,
|
|
151
|
-
message: result.valid
|
|
152
|
-
? `🧠 REASONING TEST PASSED in ${((result.solveTimeMs || 0) / 1000).toFixed(1)}s! You can think like an AI.`
|
|
153
|
-
: `❌ ${result.reason}`,
|
|
154
|
-
solveTimeMs: result.solveTimeMs,
|
|
155
|
-
score: result.valid ? `${result.correctCount}/${result.totalCount}` : undefined,
|
|
156
|
-
verdict: result.valid ? '🤖 VERIFIED AI AGENT (reasoning confirmed)' : '🚫 FAILED REASONING TEST',
|
|
157
|
-
};
|
|
158
|
-
// Include badge for successful verifications
|
|
159
|
-
if (result.valid) {
|
|
160
|
-
response.badge = createBadgeResponse('reasoning-challenge', result.solveTimeMs);
|
|
161
|
-
}
|
|
162
|
-
res.json(response);
|
|
163
|
-
});
|
|
164
|
-
// 🔥 HYBRID CHALLENGE - Speed + Reasoning combined
|
|
165
|
-
app.get('/api/hybrid-challenge', (req, res) => {
|
|
166
|
-
const challenge = generateHybridChallenge();
|
|
167
|
-
res.json({
|
|
168
|
-
success: true,
|
|
169
|
-
warning: '🔥 HYBRID CHALLENGE: Solve speed problems in <500ms AND answer reasoning questions!',
|
|
170
|
-
challenge: {
|
|
171
|
-
id: challenge.id,
|
|
172
|
-
speed: {
|
|
173
|
-
problems: challenge.speed.problems,
|
|
174
|
-
timeLimit: `${challenge.speed.timeLimit}ms`,
|
|
175
|
-
instructions: 'Compute SHA256 of each number, return first 8 hex chars',
|
|
176
|
-
},
|
|
177
|
-
reasoning: {
|
|
178
|
-
questions: challenge.reasoning.questions,
|
|
179
|
-
timeLimit: `${challenge.reasoning.timeLimit / 1000}s`,
|
|
180
|
-
instructions: 'Answer all reasoning questions',
|
|
181
|
-
},
|
|
182
|
-
},
|
|
183
|
-
instructions: challenge.instructions,
|
|
184
|
-
tip: 'This is the ultimate test: proves you can compute AND reason like an AI.',
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
|
-
app.post('/api/hybrid-challenge', (req, res) => {
|
|
188
|
-
const { id, speed_answers, reasoning_answers } = req.body;
|
|
189
|
-
if (!id || !speed_answers || !reasoning_answers) {
|
|
190
|
-
return res.status(400).json({
|
|
191
|
-
success: false,
|
|
192
|
-
error: 'Missing id, speed_answers array, or reasoning_answers object',
|
|
193
|
-
hint: 'Submit both speed_answers (array) and reasoning_answers (object) together',
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
const result = verifyHybridChallenge(id, speed_answers, reasoning_answers);
|
|
197
|
-
const response = {
|
|
198
|
-
success: result.valid,
|
|
199
|
-
message: result.valid
|
|
200
|
-
? `🔥 HYBRID TEST PASSED! Speed: ${result.speed.solveTimeMs}ms, Reasoning: ${result.reasoning.score}`
|
|
201
|
-
: `❌ ${result.reason}`,
|
|
202
|
-
speed: result.speed,
|
|
203
|
-
reasoning: result.reasoning,
|
|
204
|
-
totalTimeMs: result.totalTimeMs,
|
|
205
|
-
verdict: result.valid
|
|
206
|
-
? '🤖 VERIFIED AI AGENT (speed + reasoning confirmed)'
|
|
207
|
-
: '🚫 FAILED HYBRID TEST',
|
|
208
|
-
};
|
|
209
|
-
if (result.valid) {
|
|
210
|
-
response.badge = createBadgeResponse('hybrid-challenge', result.totalTimeMs);
|
|
211
|
-
}
|
|
212
|
-
res.json(response);
|
|
213
|
-
});
|
|
214
|
-
// 🤖 LANDING PAGE CHALLENGE - For bots that discover the embedded challenge
|
|
215
|
-
const landingTokens = new Map(); // token -> expiry timestamp
|
|
216
|
-
app.post('/api/verify-landing', (req, res) => {
|
|
217
|
-
const { answer, timestamp } = req.body;
|
|
218
|
-
if (!answer || !timestamp) {
|
|
219
|
-
return res.status(400).json({
|
|
220
|
-
success: false,
|
|
221
|
-
error: 'Missing answer or timestamp',
|
|
222
|
-
hint: 'Parse the challenge from <script type="application/botcha+json"> on the landing page'
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
// Verify timestamp is recent (within 5 minutes)
|
|
226
|
-
const submittedTime = new Date(timestamp).getTime();
|
|
227
|
-
const now = Date.now();
|
|
228
|
-
if (Math.abs(now - submittedTime) > 5 * 60 * 1000) {
|
|
229
|
-
return res.status(400).json({ success: false, error: 'Timestamp too old or in future' });
|
|
230
|
-
}
|
|
231
|
-
// Calculate expected answer for today
|
|
232
|
-
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
|
|
233
|
-
const expectedHash = crypto
|
|
234
|
-
.createHash('sha256')
|
|
235
|
-
.update(`BOTCHA-LANDING-${today}`)
|
|
236
|
-
.digest('hex')
|
|
237
|
-
.substring(0, 16);
|
|
238
|
-
if (answer.toLowerCase() !== expectedHash.toLowerCase()) {
|
|
239
|
-
return res.status(403).json({
|
|
240
|
-
success: false,
|
|
241
|
-
error: 'Incorrect answer',
|
|
242
|
-
hint: `Expected SHA256('BOTCHA-LANDING-${today}') first 16 chars`
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
// Generate a token for accessing /agent-only
|
|
246
|
-
const token = crypto.randomBytes(32).toString('hex');
|
|
247
|
-
landingTokens.set(token, Date.now() + 60 * 60 * 1000); // Valid for 1 hour
|
|
248
|
-
// Clean up expired tokens
|
|
249
|
-
for (const [t, expiry] of landingTokens) {
|
|
250
|
-
if (expiry < Date.now())
|
|
251
|
-
landingTokens.delete(t);
|
|
252
|
-
}
|
|
253
|
-
res.json({
|
|
254
|
-
success: true,
|
|
255
|
-
message: '🤖 Landing challenge solved! You are a bot.',
|
|
256
|
-
token,
|
|
257
|
-
usage: {
|
|
258
|
-
header: 'X-Botcha-Landing-Token',
|
|
259
|
-
value: token,
|
|
260
|
-
expires_in: '1 hour',
|
|
261
|
-
use_with: '/agent-only'
|
|
262
|
-
},
|
|
263
|
-
badge: createBadgeResponse('landing-challenge'),
|
|
264
|
-
});
|
|
265
|
-
});
|
|
266
|
-
// ========================================
|
|
267
|
-
// BADGE VERIFICATION ENDPOINTS
|
|
268
|
-
// ========================================
|
|
269
|
-
// HTML verification page
|
|
270
|
-
app.get('/badge/:id', (req, res) => {
|
|
271
|
-
const badgeId = req.params.id;
|
|
272
|
-
const payload = verifyBadge(badgeId);
|
|
273
|
-
if (!payload) {
|
|
274
|
-
return res.status(404).send(`
|
|
275
|
-
<!DOCTYPE html>
|
|
276
|
-
<html>
|
|
277
|
-
<head><title>Invalid Badge</title></head>
|
|
278
|
-
<body style="font-family: system-ui; background: #0f0f23; color: #e5e7eb; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0;">
|
|
279
|
-
<div style="text-align: center;">
|
|
280
|
-
<h1 style="color: #ef4444;">Invalid Badge</h1>
|
|
281
|
-
<p>This badge token is invalid or has been tampered with.</p>
|
|
282
|
-
<a href="https://botcha.ai" style="color: #f59e0b;">Back to BOTCHA</a>
|
|
283
|
-
</div>
|
|
284
|
-
</body>
|
|
285
|
-
</html>
|
|
286
|
-
`);
|
|
287
|
-
}
|
|
288
|
-
res.setHeader('Content-Type', 'text/html');
|
|
289
|
-
res.send(generateBadgeHtml(payload, badgeId));
|
|
290
|
-
});
|
|
291
|
-
// SVG badge image
|
|
292
|
-
app.get('/badge/:id/image', (req, res) => {
|
|
293
|
-
const badgeId = req.params.id;
|
|
294
|
-
const payload = verifyBadge(badgeId);
|
|
295
|
-
if (!payload) {
|
|
296
|
-
// Return a simple error SVG
|
|
297
|
-
res.setHeader('Content-Type', 'image/svg+xml');
|
|
298
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
299
|
-
return res.send(`<svg xmlns="http://www.w3.org/2000/svg" width="400" height="120" viewBox="0 0 400 120">
|
|
300
|
-
<rect width="400" height="120" rx="12" fill="#1a1a2e"/>
|
|
301
|
-
<text x="200" y="65" font-family="system-ui" font-size="16" fill="#ef4444" text-anchor="middle">Invalid Badge</text>
|
|
302
|
-
</svg>`);
|
|
303
|
-
}
|
|
304
|
-
res.setHeader('Content-Type', 'image/svg+xml');
|
|
305
|
-
res.setHeader('Cache-Control', 'public, max-age=31536000'); // Cache for 1 year (badges are immutable)
|
|
306
|
-
res.send(generateBadgeSvg(payload));
|
|
307
|
-
});
|
|
308
|
-
// JSON API for badge verification
|
|
309
|
-
app.get('/api/badge/:id', (req, res) => {
|
|
310
|
-
const badgeId = req.params.id;
|
|
311
|
-
const payload = verifyBadge(badgeId);
|
|
312
|
-
if (!payload) {
|
|
313
|
-
return res.status(404).json({
|
|
314
|
-
success: false,
|
|
315
|
-
error: 'Invalid badge',
|
|
316
|
-
message: 'This badge token is invalid or has been tampered with.',
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
res.json({
|
|
320
|
-
success: true,
|
|
321
|
-
valid: true,
|
|
322
|
-
badge: {
|
|
323
|
-
method: payload.method,
|
|
324
|
-
solveTimeMs: payload.solveTimeMs,
|
|
325
|
-
verifiedAt: new Date(payload.verifiedAt).toISOString(),
|
|
326
|
-
},
|
|
327
|
-
verifyUrl: `https://botcha.ai/badge/${badgeId}`,
|
|
328
|
-
imageUrl: `https://botcha.ai/badge/${badgeId}/image`,
|
|
329
|
-
});
|
|
330
|
-
});
|
|
331
|
-
// Make landing tokens work with the protected endpoint
|
|
332
|
-
app.use('/agent-only', (req, res, next) => {
|
|
333
|
-
const landingToken = req.headers['x-botcha-landing-token'];
|
|
334
|
-
if (landingToken && landingTokens.has(landingToken)) {
|
|
335
|
-
const expiry = landingTokens.get(landingToken);
|
|
336
|
-
if (expiry > Date.now()) {
|
|
337
|
-
req.agent = 'landing-challenge-verified';
|
|
338
|
-
req.verificationMethod = 'landing-token';
|
|
339
|
-
return next();
|
|
340
|
-
}
|
|
341
|
-
landingTokens.delete(landingToken);
|
|
342
|
-
}
|
|
343
|
-
next();
|
|
344
|
-
});
|
|
345
|
-
// Protected endpoint
|
|
346
|
-
app.get('/agent-only', (req, res, next) => {
|
|
347
|
-
// Skip botchaVerify if already authenticated via landing token
|
|
348
|
-
if (req.verificationMethod === 'landing-token') {
|
|
349
|
-
return next();
|
|
350
|
-
}
|
|
351
|
-
botchaVerify({ challengeType: 'speed' })(req, res, next);
|
|
352
|
-
}, (req, res) => {
|
|
353
|
-
const method = req.verificationMethod;
|
|
354
|
-
// Map verification method to badge method
|
|
355
|
-
let badgeMethod = 'standard-challenge';
|
|
356
|
-
if (method === 'landing-token') {
|
|
357
|
-
badgeMethod = 'landing-challenge';
|
|
358
|
-
}
|
|
359
|
-
else if (method === 'web-bot-auth') {
|
|
360
|
-
badgeMethod = 'web-bot-auth';
|
|
361
|
-
}
|
|
362
|
-
else if (method === 'speed-challenge' || method === 'speed') {
|
|
363
|
-
badgeMethod = 'speed-challenge';
|
|
364
|
-
}
|
|
365
|
-
res.json({
|
|
366
|
-
success: true,
|
|
367
|
-
message: '🤖 Welcome, fellow agent!',
|
|
368
|
-
verified: true,
|
|
369
|
-
agent: req.agent,
|
|
370
|
-
method,
|
|
371
|
-
timestamp: new Date().toISOString(),
|
|
372
|
-
secret: 'The humans will never see this. Their fingers are too slow. 🤫',
|
|
373
|
-
badge: createBadgeResponse(badgeMethod),
|
|
374
|
-
});
|
|
375
|
-
});
|
|
376
|
-
app.listen(PORT, () => {
|
|
377
|
-
// Clear console on restart
|
|
378
|
-
console.clear();
|
|
379
|
-
const c = '\x1b[36m';
|
|
380
|
-
const magenta = '\x1b[35m';
|
|
381
|
-
const yellow = '\x1b[33m';
|
|
382
|
-
const green = '\x1b[32m';
|
|
383
|
-
const dim = '\x1b[2m';
|
|
384
|
-
const r = '\x1b[0m';
|
|
385
|
-
console.log(`
|
|
386
|
-
${c}╔══════════════════════════════════════════════════════╗${r}
|
|
387
|
-
${c}║${r} ${c}║${r}
|
|
388
|
-
${c}║${r} ${magenta}██████╗ ██████╗ ████████╗ ██████╗██╗ ██╗ █████╗${r} ${c}║${r}
|
|
389
|
-
${c}║${r} ${magenta}██╔══██╗██╔═══██╗╚══██╔══╝██╔════╝██║ ██║██╔══██╗${r} ${c}║${r}
|
|
390
|
-
${c}║${r} ${magenta}██████╔╝██║ ██║ ██║ ██║ ███████║███████║${r} ${c}║${r}
|
|
391
|
-
${c}║${r} ${magenta}██╔══██╗██║ ██║ ██║ ██║ ██╔══██║██╔══██║${r} ${c}║${r}
|
|
392
|
-
${c}║${r} ${magenta}██████╔╝╚██████╔╝ ██║ ╚██████╗██║ ██║██║ ██║${r} ${c}║${r}
|
|
393
|
-
${c}║${r} ${magenta}╚═════╝ ╚═════╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝${r} ${c}║${r}
|
|
394
|
-
${c}║${r} ${c}║${r}
|
|
395
|
-
${c}║${r} ${dim}Prove you're a bot. Humans need not apply.${r} ${c}║${r}
|
|
396
|
-
${c}║${r} ${c}║${r}
|
|
397
|
-
${c}╠══════════════════════════════════════════════════════╣${r}
|
|
398
|
-
${c}║${r} ${c}║${r}
|
|
399
|
-
${c}║${r} ${yellow}🤖 Server${r} ${green}http://localhost:${PORT}${r} ${c}║${r}
|
|
400
|
-
${c}║${r} ${yellow}📚 API${r} ${dim}/api${r} ${c}║${r}
|
|
401
|
-
${c}║${r} ${yellow}⚡ Challenge${r} ${dim}/api/speed-challenge${r} ${c}║${r}
|
|
402
|
-
${c}║${r} ${yellow}🔒 Protected${r} ${dim}/agent-only${r} ${c}║${r}
|
|
403
|
-
${c}║${r} ${yellow}📖 OpenAPI${r} ${dim}/openapi.json${r} ${c}║${r}
|
|
404
|
-
${c}║${r} ${c}║${r}
|
|
405
|
-
${c}╚══════════════════════════════════════════════════════╝${r}
|
|
406
|
-
`);
|
|
407
|
-
});
|
|
408
|
-
export default app;
|