@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 CHANGED
@@ -58,34 +58,45 @@ pip install botcha
58
58
 
59
59
  ## Quick Start
60
60
 
61
- ### TypeScript/JavaScript
61
+ ### Protect Your API (Server-Side)
62
62
 
63
63
  ```typescript
64
64
  import express from 'express';
65
- import { botcha } from '@dupecom/botcha';
65
+ import { botchaVerify } from '@dupecom/botcha-verify/express';
66
66
 
67
67
  const app = express();
68
68
 
69
- // Protect any route - only AI agents can access
70
- app.get('/agent-only', botcha.verify(), (req, res) => {
71
- res.json({ message: 'Welcome, fellow AI! 🤖' });
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
- ### Python
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, solve_botcha
95
+ from botcha import BotchaClient
81
96
 
82
- # Client SDK for AI agents
83
97
  async with BotchaClient() as client:
84
- # Get verification token
85
- token = await client.get_token()
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 v0.19.0+ signs tokens with ES256 (asymmetric)** — no shared secret needed.
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
 
@@ -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.0';
3
+ const SDK_VERSION = '0.20.1';
4
4
  // Export stream client
5
5
  export { BotchaStreamClient } from './stream.js';
6
6
  /**
@@ -25,6 +25,7 @@ export interface TAPBotchaOptions {
25
25
  requireCapabilities?: string[];
26
26
  agentsKV?: any;
27
27
  sessionsKV?: any;
28
+ noncesKV?: any;
28
29
  }
29
30
  /**
30
31
  * Enhanced BOTCHA middleware with TAP support
@@ -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;CAClB;AAsBD;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,gBAAqB,IAGhD,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,wDAqE9D;AA8SD,eAAO,MAAM,cAAc;IACzB;;OAEG;uBACe,OAAO,CAAC,gBAAgB,CAAC,WAvXxB,OAAO,OAAO,QAAQ,QAAQ,YAAY;IA8X7D;;OAEG;yBACiB,OAAO,CAAC,gBAAgB,CAAC,WAjY1B,OAAO,OAAO,QAAQ,QAAQ,YAAY;IAwY7D;;OAEG;8BACsB,OAAO,CAAC,gBAAgB,CAAC,WA3Y/B,OAAO,OAAO,QAAQ,QAAQ,YAAY;IAkZ7D;;OAEG;4BACoB,OAAO,CAAC,gBAAgB,CAAC,WArZ7B,OAAO,OAAO,QAAQ,QAAQ,YAAY;CA4Z9D,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,yBAAyB,0BAAoB,CAAC;AAE3D,eAAe,iBAAiB,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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dupecom/botcha",
3
- "version": "0.20.0",
3
+ "version": "0.20.1",
4
4
  "description": "Prove you're a bot. Humans need not apply. Reverse CAPTCHA for AI-only APIs.",
5
5
  "workspaces": [
6
6
  "packages/*"
@@ -1,4 +0,0 @@
1
- import { Express } from 'express';
2
- declare const app: Express;
3
- export default app;
4
- //# sourceMappingURL=index.d.ts.map
@@ -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;