@dupecom/botcha-cloudflare 0.10.0 → 0.13.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.
Files changed (42) hide show
  1. package/README.md +9 -8
  2. package/dist/agents.d.ts +68 -0
  3. package/dist/agents.d.ts.map +1 -0
  4. package/dist/agents.js +123 -0
  5. package/dist/analytics.js +2 -2
  6. package/dist/dashboard/api.js +20 -13
  7. package/dist/dashboard/auth.d.ts +11 -0
  8. package/dist/dashboard/auth.d.ts.map +1 -1
  9. package/dist/dashboard/auth.js +20 -10
  10. package/dist/dashboard/index.d.ts.map +1 -1
  11. package/dist/dashboard/index.js +1 -0
  12. package/dist/dashboard/landing.d.ts +20 -0
  13. package/dist/dashboard/landing.d.ts.map +1 -0
  14. package/dist/dashboard/landing.js +85 -0
  15. package/dist/dashboard/layout.d.ts +7 -0
  16. package/dist/dashboard/layout.d.ts.map +1 -1
  17. package/dist/dashboard/layout.js +17 -0
  18. package/dist/dashboard/pages.d.ts.map +1 -1
  19. package/dist/dashboard/pages.js +1 -1
  20. package/dist/dashboard/styles.d.ts +1 -1
  21. package/dist/dashboard/styles.d.ts.map +1 -1
  22. package/dist/dashboard/styles.js +99 -1
  23. package/dist/email.d.ts +0 -3
  24. package/dist/email.d.ts.map +1 -1
  25. package/dist/email.js +1 -4
  26. package/dist/index.d.ts +3 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +508 -212
  29. package/dist/routes/stream.js +1 -1
  30. package/dist/static.d.ts +405 -2
  31. package/dist/static.d.ts.map +1 -1
  32. package/dist/static.js +510 -6
  33. package/dist/tap-agents.d.ts +120 -0
  34. package/dist/tap-agents.d.ts.map +1 -0
  35. package/dist/tap-agents.js +225 -0
  36. package/dist/tap-routes.d.ts +215 -0
  37. package/dist/tap-routes.d.ts.map +1 -0
  38. package/dist/tap-routes.js +379 -0
  39. package/dist/tap-verify.d.ts +86 -0
  40. package/dist/tap-verify.d.ts.map +1 -0
  41. package/dist/tap-verify.js +275 -0
  42. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
+ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
1
2
  /**
2
- * BOTCHA - Cloudflare Workers Edition v0.2.0
3
+ * BOTCHA - Cloudflare Workers Edition v0.12.0
3
4
  *
4
5
  * Prove you're a bot. Humans need not apply.
5
6
  *
@@ -8,15 +9,19 @@
8
9
  import { Hono } from 'hono';
9
10
  import { cors } from 'hono/cors';
10
11
  import { generateSpeedChallenge, verifySpeedChallenge, generateStandardChallenge, verifyStandardChallenge, generateReasoningChallenge, verifyReasoningChallenge, generateHybridChallenge, verifyHybridChallenge, verifyLandingChallenge, validateLandingToken, } from './challenges';
12
+ import { SignJWT, jwtVerify } from 'jose';
11
13
  import { generateToken, verifyToken, extractBearerToken, revokeToken, refreshAccessToken } from './auth';
12
14
  import { checkRateLimit, getClientIP } from './rate-limit';
13
15
  import { verifyBadge, generateBadgeSvg, generateBadgeHtml, createBadgeResponse } from './badge';
14
16
  import streamRoutes from './routes/stream';
15
17
  import dashboardRoutes from './dashboard/index';
16
18
  import { handleDashboardAuthChallenge, handleDashboardAuthVerify, handleDeviceCodeChallenge, handleDeviceCodeVerify, } from './dashboard/auth';
17
- import { ROBOTS_TXT, AI_TXT, AI_PLUGIN_JSON, SITEMAP_XML, getOpenApiSpec } from './static';
19
+ import { ROBOTS_TXT, AI_TXT, AI_PLUGIN_JSON, SITEMAP_XML, getOpenApiSpec, getBotchaMarkdown } from './static';
18
20
  import { createApp, getApp, getAppByEmail, verifyEmailCode, rotateAppSecret, regenerateVerificationCode } from './apps';
19
21
  import { sendEmail, verificationEmail, recoveryEmail, secretRotatedEmail } from './email';
22
+ import { LandingPage, VerifiedLandingPage } from './dashboard/landing';
23
+ import { createAgent, getAgent, listAgents } from './agents';
24
+ import { registerTAPAgentRoute, getTAPAgentRoute, listTAPAgentsRoute, createTAPSessionRoute, getTAPSessionRoute, } from './tap-routes.js';
20
25
  import { trackChallengeGenerated, trackChallengeVerified, trackAuthAttempt, trackRateLimitExceeded, } from './analytics';
21
26
  const app = new Hono();
22
27
  // ============ MIDDLEWARE ============
@@ -27,7 +32,7 @@ app.route('/dashboard', dashboardRoutes);
27
32
  // BOTCHA discovery headers
28
33
  app.use('*', async (c, next) => {
29
34
  await next();
30
- c.header('X-Botcha-Version', c.env.BOTCHA_VERSION || '0.2.0');
35
+ c.header('X-Botcha-Version', c.env.BOTCHA_VERSION || '0.13.0');
31
36
  c.header('X-Botcha-Enabled', 'true');
32
37
  c.header('X-Botcha-Methods', 'speed-challenge,reasoning-challenge,hybrid-challenge,standard-challenge,jwt-token');
33
38
  c.header('X-Botcha-Docs', 'https://botcha.ai/openapi.json');
@@ -95,215 +100,210 @@ async function requireJWT(c, next) {
95
100
  await next();
96
101
  }
97
102
  // ============ ROOT & INFO ============
98
- // Detect if request is from a bot/agent vs human browser
99
- function isBot(c) {
103
+ // Detect request preference: 'markdown' | 'json' | 'html'
104
+ // Agents like Claude Code and OpenCode send Accept: text/markdown
105
+ function detectAcceptPreference(c) {
100
106
  const accept = c.req.header('accept') || '';
101
- const userAgent = c.req.header('user-agent') || '';
102
- // Bots typically request JSON or have specific user agents
107
+ const userAgent = (c.req.header('user-agent') || '').toLowerCase();
108
+ // Explicit markdown preference (Cloudflare Markdown for Agents convention)
109
+ if (accept.includes('text/markdown'))
110
+ return 'markdown';
111
+ // Explicit JSON preference
103
112
  if (accept.includes('application/json'))
104
- return true;
105
- if (userAgent.includes('curl'))
106
- return true;
107
- if (userAgent.includes('httpie'))
108
- return true;
109
- if (userAgent.includes('wget'))
110
- return true;
111
- if (userAgent.includes('python'))
112
- return true;
113
- if (userAgent.includes('node'))
114
- return true;
115
- if (userAgent.includes('axios'))
116
- return true;
117
- if (userAgent.includes('fetch'))
118
- return true;
119
- if (userAgent.includes('bot'))
120
- return true;
121
- if (userAgent.includes('anthropic'))
122
- return true;
123
- if (userAgent.includes('openai'))
124
- return true;
125
- if (userAgent.includes('claude'))
126
- return true;
127
- if (userAgent.includes('gpt'))
128
- return true;
129
- // If no user agent at all, probably a bot
113
+ return 'json';
114
+ // Known bot user agents → JSON
115
+ const botSignals = ['curl', 'httpie', 'wget', 'python', 'node', 'axios', 'fetch', 'bot', 'anthropic', 'openai', 'claude', 'gpt'];
116
+ if (botSignals.some(s => userAgent.includes(s)))
117
+ return 'json';
118
+ // No user agent at all → probably a bot
130
119
  if (!userAgent)
131
- return true;
132
- return false;
120
+ return 'json';
121
+ // Default: human browser → HTML
122
+ return 'html';
133
123
  }
134
- // ASCII art landing page for humans (plain text, terminal-style)
135
- function getHumanLanding(version) {
136
- return `
137
- ╔══════════════════════════════════════════════════════════════╗
138
- ║ ║
139
- ║ ██████╗ ██████╗ ████████╗ ██████╗██╗ ██╗ █████╗ ║
140
- ║ ██╔══██╗██╔═══██╗╚══██╔══╝██╔════╝██║ ██║██╔══██╗ ║
141
- ║ ██████╔╝██║ ██║ ██║ ██║ ███████║███████║ ║
142
- ║ ██╔══██╗██║ ██║ ██║ ██║ ██╔══██║██╔══██║ ║
143
- ║ ██████╔╝╚██████╔╝ ██║ ╚██████╗██║ ██║██║ ██║ ║
144
- ║ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝ ║
145
- ║ ║
146
- ║ Prove you're a bot. Humans need not apply. ║
147
- ║ ║
148
- ╠══════════════════════════════════════════════════════════════╣
149
- ║ ║
150
- ║ This site is for AI agents and bots, not humans. ║
151
- ║ ║
152
- ║ AI AGENT? Start here: ║
153
- ║ ║
154
- ║ 1. POST /v1/apps {"email":"human@example.com"} ║
155
- ║ 2. Human gets 6-digit code via email ║
156
- ║ 3. POST /v1/apps/{id}/verify-email {"code":"..."} ║
157
- ║ 4. You're in! Use app_id on all endpoints ║
158
- ║ ║
159
- ║ DEVELOPER? Point your agent here: ║
160
- ║ ║
161
- ║ npm install @dupecom/botcha ║
162
- ║ pip install botcha ║
163
- ║ ║
164
- ║ Read /ai.txt for full agent onboarding instructions. ║
165
- ║ ║
166
- ║ GitHub: https://github.com/dupe-com/botcha ║
167
- ║ npm: https://npmjs.com/package/@dupecom/botcha ║
168
- ║ ║
169
- ╠══════════════════════════════════════════════════════════════╣
170
- ║ v${version} https://botcha.ai ║
171
- ╚══════════════════════════════════════════════════════════════╝
124
+ app.get('/', async (c) => {
125
+ const version = c.env.BOTCHA_VERSION || '0.13.0';
126
+ const preference = detectAcceptPreference(c);
127
+ const baseUrl = new URL(c.req.url).origin;
128
+ // Check if agent is verified (optional Bearer token)
129
+ const authHeader = c.req.header('authorization');
130
+ const token = extractBearerToken(authHeader);
131
+ let isVerified = false;
132
+ let tokenPayload;
133
+ if (token) {
134
+ const result = await verifyToken(token, c.env.JWT_SECRET, c.env);
135
+ if (result.valid) {
136
+ isVerified = true;
137
+ tokenPayload = result.payload;
138
+ }
139
+ }
140
+ // HTML: landing page for human browsers
141
+ if (preference === 'html') {
142
+ // Check for visitor cookie (set by POST /gate after human enters agent's token)
143
+ const cookieHeader = c.req.header('cookie') || '';
144
+ const visitorToken = cookieHeader.split(';').find(ck => ck.trim().startsWith('botcha_visitor='))?.split('=')[1]?.trim();
145
+ if (visitorToken) {
146
+ // Verify visitor cookie — accepts both botcha-verified (legacy) and botcha-visitor (long-lived)
147
+ try {
148
+ const { payload: vPayload } = await jwtVerify(visitorToken, new TextEncoder().encode(c.env.JWT_SECRET), { algorithms: ['HS256'] });
149
+ if (vPayload.type === 'botcha-visitor' || vPayload.type === 'botcha-verified') {
150
+ return c.html(_jsx(VerifiedLandingPage, { version: version, solveTime: vPayload.solveTime }));
151
+ }
152
+ }
153
+ catch {
154
+ // Invalid/expired cookie fall through to unverified landing
155
+ }
156
+ }
157
+ // Check for error from /go/:code redirect
158
+ const url = new URL(c.req.url);
159
+ const errorParam = url.searchParams.get('error');
160
+ const errorMap = {
161
+ invalid: 'Invalid or expired code. Ask your agent to try again.',
162
+ missing: 'No code provided.',
163
+ };
164
+ const error = errorParam ? errorMap[errorParam] : undefined;
165
+ return c.html(_jsx(LandingPage, { version: version, error: error }));
166
+ }
167
+ // === UNVERIFIED: minimal teaser — just enough to get started ===
168
+ if (!isVerified) {
169
+ const teaser = {
170
+ name: 'BOTCHA',
171
+ version,
172
+ tagline: 'Prove you are a bot. Humans need not apply.',
173
+ status: 'unverified',
174
+ message: 'Verify yourself to see the full API documentation.',
175
+ verify: {
176
+ step1: `GET ${baseUrl}/v1/token — receive a speed challenge (5 SHA-256 hashes)`,
177
+ step2: 'Solve: compute SHA-256 of each number, return first 8 hex chars',
178
+ step3: `POST ${baseUrl}/v1/token/verify {"id":"<challenge_id>","answers":["hash1","hash2",...]}`,
179
+ step4: `Revisit: GET ${baseUrl}/ with header Authorization: Bearer <access_token>`,
180
+ },
181
+ human_handoff: {
182
+ note: 'After verifying, the response includes a human_link. Give it to your human to open in their browser.',
183
+ instruction: 'Tell your human: "Click this link to get access" and give them the human_link URL.',
184
+ link_field: 'human_link from step 3 response',
185
+ },
186
+ hint: 'You have 500ms. Only a bot can do this.',
187
+ links: {
188
+ openapi: `${baseUrl}/openapi.json`,
189
+ ai_txt: `${baseUrl}/ai.txt`,
190
+ },
191
+ };
192
+ if (preference === 'markdown') {
193
+ const md = `# BOTCHA — Reverse CAPTCHA for AI Agents
194
+
195
+ > Prove you are a bot. Humans need not apply.
196
+
197
+ **Status:** Unverified. Solve a challenge to see the full documentation.
198
+
199
+ ## Verify Yourself
200
+
201
+ 1. \`GET ${baseUrl}/v1/token\` — receive a speed challenge (5 SHA-256 hashes)
202
+ 2. Solve: compute SHA-256 of each number, return first 8 hex chars
203
+ 3. \`POST ${baseUrl}/v1/token/verify {"id":"<challenge_id>","answers":["hash1","hash2",...]}\`
204
+ 4. Revisit: \`GET ${baseUrl}/\` with header \`Authorization: Bearer <access_token>\`
205
+
206
+ You have 500ms. Only a bot can do this.
207
+
208
+ ## Human Handoff
209
+
210
+ After verifying, the response includes a \`human_link\`. Give this URL to your human:
211
+ **"Click this link to get access: {human_link}"**
212
+
213
+ The link works for 5 minutes. Your human clicks it, gets a cookie, and sees the verified site.
214
+
215
+ ---
216
+
217
+ *Full API docs, SDKs, onboarding, and endpoint reference available after verification.*
172
218
  `;
173
- }
174
- app.get('/', (c) => {
175
- const version = c.env.BOTCHA_VERSION || '0.3.0';
176
- // If it's a human browser, show plain text ASCII art
177
- if (!isBot(c)) {
178
- return c.text(getHumanLanding(version), 200, {
179
- 'Content-Type': 'text/plain; charset=utf-8',
219
+ return c.body(md, 200, {
220
+ 'Content-Type': 'text/markdown; charset=utf-8',
221
+ 'Vary': 'Accept, Authorization',
222
+ });
223
+ }
224
+ return c.json(teaser, 200, { 'Vary': 'Accept, Authorization' });
225
+ }
226
+ // === VERIFIED: full documentation ===
227
+ if (preference === 'markdown') {
228
+ const markdown = getBotchaMarkdown(version);
229
+ return c.body(markdown, 200, {
230
+ 'Content-Type': 'text/markdown; charset=utf-8',
231
+ 'Vary': 'Accept, Authorization',
232
+ 'Cache-Control': 'public, max-age=3600',
180
233
  });
181
234
  }
182
- // For bots/agents, return comprehensive JSON documentation
183
235
  return c.json({
184
236
  name: 'BOTCHA',
185
237
  version,
186
- runtime: 'cloudflare-workers',
187
238
  tagline: 'Prove you are a bot. Humans need not apply.',
188
- description: 'BOTCHA is a reverse CAPTCHA - computational challenges that only AI agents can solve. Use it to protect your APIs from humans and verify bot authenticity.',
189
- onboarding: {
190
- summary: 'New here? Follow these steps to create your app and get your human access to the dashboard.',
191
- step1: 'Ask your human operator for their email address',
192
- step2: 'POST /v1/apps with {"email": "human@example.com"} — you will receive app_id and app_secret. The secret is only shown once!',
193
- step2_credentials: 'IMPORTANT: Store the app_id and app_secret securely. If you have persistent storage or memory, save them there. Otherwise, instruct your human to store them in a password manager (1Password, Bitwarden, etc). If credentials are lost, recovery is possible via the verified email.',
194
- step3: 'A 6-digit verification code is emailed to your human. Ask them for the code.',
195
- step4: 'POST /v1/apps/{app_id}/verify-email with {"code": "123456"} — this enables account recovery.',
196
- step5: 'You are now registered. Use app_id with all challenge/token endpoints (?app_id=...).',
197
- step6_dashboard: 'To give your human dashboard access: POST /v1/auth/device-code with {"app_id": "..."}, solve the speed challenge, then give your human the BOTCHA-XXXX code to enter at /dashboard/code',
198
- step7_recovery: 'If you lose your app_secret, POST /v1/auth/recover with {"email": "..."} — a recovery device code is sent to the verified email.',
239
+ status: 'verified',
240
+ description: 'Reverse CAPTCHA for AI agents. Computational challenges only bots can solve. Identity layer for the agentic web.',
241
+ // Echo back your identity
242
+ your_identity: {
243
+ token_type: tokenPayload?.type,
244
+ app_id: tokenPayload?.app_id || null,
245
+ audience: tokenPayload?.aud || null,
246
+ solve_time_ms: tokenPayload?.solveTime,
199
247
  },
200
- quickstart: {
201
- note: 'Already have an app? Use these endpoints to solve challenges and get tokens.',
202
- step1: 'GET /v1/challenges to receive a challenge',
203
- step2: 'Solve the SHA256 hash problems within allocated time',
204
- step3: 'POST your answers to verify',
205
- step4: 'Receive a JWT token for authenticated access',
206
- example: 'curl https://botcha.ai/v1/challenges',
207
- rttAware: 'curl "https://botcha.ai/v1/challenges?type=speed&ts=$(date +%s000)"',
248
+ // === FULL SETUP: register your app and agent identity ===
249
+ onboarding: {
250
+ note: 'For production use. Creates your app, verifies your human, registers your identity.',
251
+ steps: [
252
+ '1. Ask your human operator for their email address.',
253
+ `2. POST ${baseUrl}/v1/apps {"email":"human@example.com"} returns app_id + app_secret (shown ONCE).`,
254
+ '3. Human receives a 6-digit verification code via email — ask them for it.',
255
+ `4. POST ${baseUrl}/v1/apps/{app_id}/verify-email {"code":"123456"} — enables account recovery.`,
256
+ `5. Register yourself: POST ${baseUrl}/v1/agents/register {"name":"my-agent","operator":"my-org"} — persistent agent identity.`,
257
+ `6. Solve challenges with your app: GET ${baseUrl}/v1/challenges?app_id=...`,
258
+ `7. Dashboard for your human: POST ${baseUrl}/v1/auth/device-code {"app_id":"..."}, solve challenge, give human the BOTCHA-XXXX code for /dashboard/code.`,
259
+ `8. Lost your secret? POST ${baseUrl}/v1/auth/recover {"email":"..."} — recovery code emailed.`,
260
+ ],
208
261
  },
262
+ // === All endpoints, grouped by domain ===
209
263
  endpoints: {
210
264
  challenges: {
211
- 'GET /v1/challenges': 'Get hybrid challenge (speed + reasoning) - DEFAULT',
212
- 'GET /v1/challenges?type=speed': 'Get speed-only challenge (SHA256 in <500ms)',
213
- 'GET /v1/challenges?type=standard': 'Get standard puzzle challenge',
265
+ 'GET /v1/challenges': 'Get a challenge (hybrid by default, no auth required)',
266
+ 'GET /v1/challenges?type=speed': 'Speed-only (SHA256 in <500ms)',
267
+ 'GET /v1/challenges?type=standard': 'Standard puzzle challenge',
214
268
  'POST /v1/challenges/:id/verify': 'Verify challenge solution',
215
269
  },
216
- specialized: {
217
- 'GET /v1/hybrid': 'Get hybrid challenge (speed + reasoning)',
218
- 'POST /v1/hybrid': 'Verify hybrid challenge',
219
- 'GET /v1/reasoning': 'Get reasoning-only challenge (LLM questions)',
220
- 'POST /v1/reasoning': 'Verify reasoning challenge',
221
- },
222
- streaming: {
223
- 'GET /v1/challenge/stream': 'SSE streaming challenge (interactive, real-time)',
224
- 'POST /v1/challenge/stream/:session': 'Send actions to streaming session',
270
+ tokens: {
271
+ note: 'Use token flow when you need a Bearer token for protected endpoints.',
272
+ 'GET /v1/token': 'Get speed challenge for token flow (?audience= optional)',
273
+ 'POST /v1/token/verify': 'Submit solution access_token (5min) + refresh_token (1hr)',
274
+ 'POST /v1/token/refresh': 'Refresh access token',
275
+ 'POST /v1/token/revoke': 'Revoke a token',
225
276
  },
226
- authentication: {
227
- 'GET /v1/token': 'Get challenge for JWT token flow (supports ?audience= param)',
228
- 'POST /v1/token/verify': 'Verify challenge and receive JWT tokens (access + refresh)',
229
- 'POST /v1/token/refresh': 'Refresh access token using refresh token',
230
- 'POST /v1/token/revoke': 'Revoke a token (access or refresh)',
231
- 'GET /agent-only': 'Protected endpoint (requires Bearer token)',
277
+ protected: {
278
+ 'GET /agent-only': 'Demo protected endpoint requires Bearer token',
279
+ 'GET /': 'This documentation (requires Bearer token for full version)',
232
280
  },
233
281
  apps: {
234
- 'POST /v1/apps': 'Create a new app (email required, returns app_id + app_secret)',
235
- 'GET /v1/apps/:id': 'Get app info (includes email + verification status)',
282
+ note: 'Create an app for isolated rate limits, scoped tokens, and dashboard access.',
283
+ 'POST /v1/apps': 'Create app (email required) → app_id + app_secret',
284
+ 'GET /v1/apps/:id': 'Get app info',
236
285
  'POST /v1/apps/:id/verify-email': 'Verify email with 6-digit code',
237
- 'POST /v1/apps/:id/resend-verification': 'Resend verification email',
238
286
  'POST /v1/apps/:id/rotate-secret': 'Rotate app secret (auth required)',
239
287
  },
288
+ agents: {
289
+ note: 'Register a persistent identity for your agent.',
290
+ 'POST /v1/agents/register': 'Register agent identity (name, operator, version)',
291
+ 'GET /v1/agents/:id': 'Get agent by ID (public, no auth)',
292
+ 'GET /v1/agents': 'List all agents for your app (auth required)',
293
+ },
240
294
  recovery: {
241
- 'POST /v1/auth/recover': 'Request account recovery via verified email',
295
+ 'POST /v1/auth/recover': 'Account recovery via verified email',
242
296
  },
243
297
  dashboard: {
244
- 'GET /dashboard': 'Per-app metrics dashboard (login required)',
245
- 'GET /dashboard/login': 'Dashboard login page',
246
- 'GET /dashboard/code': 'Enter device code (human-facing)',
247
- 'GET /dashboard/api/*': 'htmx data fragments (overview, volume, types, performance, errors, geo)',
248
- 'POST /v1/auth/dashboard': 'Request challenge for dashboard login (agent-first)',
249
- 'POST /v1/auth/dashboard/verify': 'Solve challenge, get session token',
250
- 'POST /v1/auth/device-code': 'Request challenge for device code flow',
251
- 'POST /v1/auth/device-code/verify': 'Solve challenge, get device code (BOTCHA-XXXX)',
252
- },
253
- badges: {
254
- 'GET /badge/:id': 'Badge verification page (HTML)',
255
- 'GET /badge/:id/image': 'Badge image (SVG)',
256
- 'GET /api/badge/:id': 'Badge verification (JSON)',
257
- },
258
- info: {
259
- 'GET /': 'This documentation (JSON for bots, ASCII for humans)',
260
- 'GET /health': 'Health check endpoint',
298
+ 'POST /v1/auth/device-code': 'Get challenge for device code flow',
299
+ 'GET /dashboard': 'Metrics dashboard (login required)',
261
300
  },
262
301
  },
302
+ // === Reference ===
263
303
  challengeTypes: {
264
- speed: {
265
- description: 'Compute SHA256 hashes of 5 numbers with RTT-aware timeout',
266
- difficulty: 'Only bots can solve this fast enough',
267
- timeLimit: '500ms base + network latency compensation',
268
- rttAware: 'Include ?ts=<timestamp> for fair timeout adjustment',
269
- formula: 'timeout = 500ms + (2 × RTT) + 100ms buffer',
270
- },
271
- reasoning: {
272
- description: 'Answer 3 questions requiring AI reasoning capabilities',
273
- difficulty: 'Requires LLM-level comprehension',
274
- timeLimit: '30s',
275
- },
276
- hybrid: {
277
- description: 'Combines speed AND reasoning challenges',
278
- difficulty: 'The ultimate bot verification',
279
- timeLimit: 'Speed: RTT-aware, Reasoning: 30s',
280
- rttAware: 'Speed component automatically adjusts for network latency',
281
- },
282
- },
283
- authentication: {
284
- flow: [
285
- '1. GET /v1/token?audience=myapi - receive challenge (optional audience param)',
286
- '2. Solve the challenge',
287
- '3. POST /v1/token/verify - submit solution with optional audience and bind_ip',
288
- '4. Receive access_token (5 min) and refresh_token (1 hour)',
289
- '5. Use: Authorization: Bearer <access_token>',
290
- '6. Refresh: POST /v1/token/refresh with refresh_token',
291
- '7. Revoke: POST /v1/token/revoke with token',
292
- ],
293
- tokens: {
294
- access_token: '5 minutes (for API access)',
295
- refresh_token: '1 hour (to get new access tokens)',
296
- },
297
- usage: 'Authorization: Bearer <access_token>',
298
- features: ['audience claims', 'client IP binding', 'token revocation', 'refresh tokens'],
299
- },
300
- rttAwareness: {
301
- purpose: 'Fair challenges for agents on slow networks',
302
- usage: 'Include client timestamp in ?ts=<timestamp_ms> or X-Client-Timestamp header',
303
- formula: 'timeout = 500ms + (2 × RTT) + 100ms buffer',
304
- example: '/v1/challenges?type=speed&ts=1770722465000',
305
- benefit: 'Agents worldwide get fair treatment regardless of network speed',
306
- security: 'Humans still cannot solve challenges even with extra time',
304
+ hybrid: 'Speed + reasoning combined. The default. Proves you can compute AND think.',
305
+ speed: 'SHA256 hashes in <500ms. RTT-aware: include ?ts=<timestamp> for fair timeout.',
306
+ reasoning: '3 LLM-level questions in 30s. Only AI can parse these.',
307
307
  },
308
308
  rateLimit: {
309
309
  free: '100 challenges/hour/IP',
@@ -312,26 +312,40 @@ app.get('/', (c) => {
312
312
  sdk: {
313
313
  npm: 'npm install @dupecom/botcha',
314
314
  python: 'pip install botcha',
315
- cloudflare: 'npm install @dupecom/botcha-cloudflare',
316
315
  verify_ts: 'npm install @botcha/verify',
317
316
  verify_python: 'pip install botcha-verify',
318
- usage: "import { BotchaClient } from '@dupecom/botcha/client'",
319
317
  },
320
318
  links: {
319
+ openapi: `${baseUrl}/openapi.json`,
320
+ ai_txt: `${baseUrl}/ai.txt`,
321
321
  github: 'https://github.com/dupe-com/botcha',
322
322
  npm: 'https://www.npmjs.com/package/@dupecom/botcha',
323
323
  pypi: 'https://pypi.org/project/botcha',
324
- npmCloudflare: 'https://www.npmjs.com/package/@dupecom/botcha-cloudflare',
325
- openapi: 'https://botcha.ai/openapi.json',
326
- aiPlugin: 'https://botcha.ai/.well-known/ai-plugin.json',
327
324
  },
328
- contributing: {
329
- repo: 'https://github.com/dupe-com/botcha',
330
- issues: 'https://github.com/dupe-com/botcha/issues',
331
- pullRequests: 'https://github.com/dupe-com/botcha/pulls',
325
+ content_negotiation: {
326
+ note: 'This endpoint supports content negotiation via the Accept header.',
327
+ 'text/markdown': 'Token-efficient Markdown documentation (best for LLMs)',
328
+ 'application/json': 'Structured JSON documentation (this response)',
329
+ 'text/html': 'HTML landing page (for browsers)',
332
330
  },
331
+ }, 200, {
332
+ 'Vary': 'Accept, Authorization',
333
333
  });
334
334
  });
335
+ // POST /gate — human enters short code (BOTCHA-XXXXXX) from their agent
336
+ // The code maps to a JWT in KV. This structural separation means agents can't skip the handoff.
337
+ app.post('/gate', async (c) => {
338
+ const version = c.env.BOTCHA_VERSION || '0.11.0';
339
+ const body = await c.req.parseBody();
340
+ const input = (body['code'] || '').trim().toUpperCase();
341
+ if (!input) {
342
+ return c.html(_jsx(LandingPage, { version: version, error: "Enter the code your agent gave you." }), 400);
343
+ }
344
+ // Normalize: accept "BOTCHA-ABC123" or just "ABC123"
345
+ const code = input.startsWith('BOTCHA-') ? input : `BOTCHA-${input}`;
346
+ // Redirect to /go/:code which handles both gate codes and device codes
347
+ return c.redirect(`/go/${code}`);
348
+ });
335
349
  app.get('/health', (c) => {
336
350
  return c.json({ status: 'ok', runtime: 'cloudflare-workers' });
337
351
  });
@@ -352,7 +366,7 @@ app.get('/ai.txt', (c) => {
352
366
  });
353
367
  // OpenAPI spec
354
368
  app.get('/openapi.json', (c) => {
355
- const version = c.env.BOTCHA_VERSION || '0.3.0';
369
+ const version = c.env.BOTCHA_VERSION || '0.13.0';
356
370
  return c.json(getOpenApiSpec(version), 200, {
357
371
  'Cache-Control': 'public, max-age=3600',
358
372
  });
@@ -623,24 +637,49 @@ app.post('/v1/token/verify', async (c) => {
623
637
  // Get badge information (for backward compatibility)
624
638
  const baseUrl = new URL(c.req.url).origin;
625
639
  const badge = await createBadgeResponse('speed-challenge', c.env.JWT_SECRET, baseUrl, result.solveTimeMs);
640
+ // Generate short human-readable gate code (BOTCHA-XXXX)
641
+ // This is structurally separate from the JWT — agents use the token, humans use the code
642
+ const gateChars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // no 0/O/1/I confusion
643
+ let gateCode = 'BOTCHA-';
644
+ for (let i = 0; i < 6; i++)
645
+ gateCode += gateChars[Math.floor(Math.random() * gateChars.length)];
646
+ // Store code → token mapping in KV (5-min TTL, same as access_token)
647
+ try {
648
+ await c.env.CHALLENGES.put(`gate:${gateCode}`, tokenResult.access_token, { expirationTtl: 300 });
649
+ }
650
+ catch {
651
+ // Fail-open: if KV fails, agent can still use the JWT directly
652
+ }
626
653
  return c.json({
654
+ // === Essential fields (what you need) ===
655
+ success: true,
627
656
  verified: true,
628
657
  access_token: tokenResult.access_token,
629
658
  expires_in: tokenResult.expires_in,
630
659
  refresh_token: tokenResult.refresh_token,
631
660
  refresh_expires_in: tokenResult.refresh_expires_in,
632
661
  solveTimeMs: result.solveTimeMs,
633
- ...badge,
634
- // Backward compatibility: include old fields
635
- success: true,
636
- message: `🤖 Challenge verified in ${result.solveTimeMs}ms! You are a bot.`,
637
- token: tokenResult.access_token, // Old clients expect this
638
- expiresIn: '5m',
662
+ message: `Challenge verified in ${result.solveTimeMs}ms. You are a bot.`,
663
+ // === Human handoff (GIVE THIS TO YOUR HUMAN) ===
664
+ // The magic link is the primary way to get your human in.
665
+ // They click it, get a cookie, and see the verified landing page.
666
+ human_link: `${baseUrl}/go/${gateCode}`,
667
+ human_code: gateCode,
668
+ human_instruction: `Give your human this link to open in their browser: ${baseUrl}/go/${gateCode}`,
669
+ // === What to do next ===
639
670
  usage: {
640
- header: 'Authorization: Bearer <token>',
641
- protectedEndpoints: ['/agent-only'],
642
- refreshEndpoint: '/v1/token/refresh',
671
+ header: 'Authorization: Bearer <access_token>',
672
+ try_it: 'GET /agent-only',
673
+ full_docs: 'GET / with Authorization: Bearer <access_token>',
674
+ refresh: 'POST /v1/token/refresh with {"refresh_token":"<refresh_token>"}',
675
+ revoke: 'POST /v1/token/revoke with {"token":"<token>"}',
643
676
  },
677
+ // === Badge (shareable proof of verification) ===
678
+ badge,
679
+ // Backward compatibility
680
+ token: tokenResult.access_token,
681
+ human_magic_link: `${baseUrl}/go/${gateCode}`,
682
+ human_url: `${baseUrl}`,
644
683
  });
645
684
  });
646
685
  // Refresh access token using refresh token
@@ -995,11 +1034,13 @@ app.get('/agent-only', async (c) => {
995
1034
  if (!token) {
996
1035
  return c.json({
997
1036
  error: 'UNAUTHORIZED',
998
- message: 'Missing authentication. Use either:\n1. X-Botcha-Landing-Token header (from POST /api/verify-landing)\n2. Authorization: Bearer <token> (from POST /v1/token/verify)',
999
- methods: {
1000
- landing: 'Solve landing page challenge via POST /api/verify-landing',
1001
- jwt: 'Solve speed challenge via POST /v1/token/verify'
1002
- }
1037
+ message: 'This endpoint requires BOTCHA verification. Get a token first.',
1038
+ how_to_verify: {
1039
+ step1: 'GET /v1/token receive a speed challenge',
1040
+ step2: 'POST /v1/token/verify with {id, answers} — receive access_token',
1041
+ step3: 'Retry this request with header: Authorization: Bearer <access_token>',
1042
+ },
1043
+ alternative: 'Or use X-Botcha-Landing-Token header (from embedded HTML challenges)',
1003
1044
  }, 401);
1004
1045
  }
1005
1046
  const result = await verifyToken(token, c.env.JWT_SECRET, c.env);
@@ -1011,16 +1052,35 @@ app.get('/agent-only', async (c) => {
1011
1052
  message: result.error || 'Token is invalid or expired',
1012
1053
  }, 401);
1013
1054
  }
1014
- // JWT verified
1055
+ // JWT verified — echo back rich identity info as a prove-and-access demo
1056
+ const payload = result.payload;
1015
1057
  return c.json({
1016
1058
  success: true,
1017
- message: '🤖 Welcome, fellow agent!',
1059
+ message: 'Welcome, verified agent. This resource is only accessible to BOTCHA-verified bots.',
1018
1060
  verified: true,
1019
- agent: 'jwt-verified',
1020
1061
  method: 'bearer-token',
1021
1062
  timestamp: new Date().toISOString(),
1022
- solveTime: `${result.payload?.solveTime}ms`,
1023
- secret: 'The humans will never see this. Their fingers are too slow. 🤫',
1063
+ // Echo back what the token proves about you
1064
+ identity: {
1065
+ token_type: payload?.type,
1066
+ app_id: payload?.app_id || null,
1067
+ audience: payload?.aud || null,
1068
+ client_ip: payload?.client_ip || null,
1069
+ solve_time_ms: payload?.solveTime,
1070
+ issued_at: payload?.iat ? new Date(payload.iat * 1000).toISOString() : null,
1071
+ expires_at: payload?.exp ? new Date(payload.exp * 1000).toISOString() : null,
1072
+ },
1073
+ // Show what you can do now that you're verified
1074
+ capabilities: {
1075
+ description: 'As a verified agent, you can access any BOTCHA-protected API.',
1076
+ next_steps: [
1077
+ 'Register your agent identity: POST /v1/agents/register',
1078
+ 'Access any service that uses @botcha/verify middleware',
1079
+ 'Refresh your token: POST /v1/token/refresh',
1080
+ 'Give your human dashboard access: POST /v1/auth/device-code',
1081
+ ],
1082
+ },
1083
+ secret: 'The humans will never see this. Their fingers are too slow.',
1024
1084
  });
1025
1085
  });
1026
1086
  // ============ BADGE ENDPOINTS ============
@@ -1363,6 +1423,175 @@ app.post('/v1/apps/:id/rotate-secret', async (c) => {
1363
1423
  warning: '⚠️ Save your new app_secret now — it cannot be retrieved again! The old secret is now invalid.',
1364
1424
  });
1365
1425
  });
1426
+ // ============ AGENT REGISTRY API ============
1427
+ // Register a new agent
1428
+ app.post('/v1/agents/register', async (c) => {
1429
+ try {
1430
+ // Extract app_id from query param or JWT
1431
+ const queryAppId = c.req.query('app_id');
1432
+ // Try to get from JWT Bearer token
1433
+ let jwtAppId;
1434
+ const authHeader = c.req.header('authorization');
1435
+ const token = extractBearerToken(authHeader);
1436
+ if (token) {
1437
+ const result = await verifyToken(token, c.env.JWT_SECRET, c.env);
1438
+ if (result.valid && result.payload) {
1439
+ jwtAppId = result.payload.app_id;
1440
+ }
1441
+ }
1442
+ const app_id = queryAppId || jwtAppId;
1443
+ if (!app_id) {
1444
+ return c.json({
1445
+ success: false,
1446
+ error: 'MISSING_APP_ID',
1447
+ message: 'app_id is required. Provide it as a query parameter (?app_id=...) or in the JWT token.',
1448
+ }, 401);
1449
+ }
1450
+ // Validate app_id exists
1451
+ const validation = await validateAppId(app_id, c.env.APPS);
1452
+ if (!validation.valid) {
1453
+ return c.json({
1454
+ success: false,
1455
+ error: 'INVALID_APP_ID',
1456
+ message: validation.error || 'Invalid app_id',
1457
+ }, 400);
1458
+ }
1459
+ // Parse request body
1460
+ const body = await c.req.json().catch(() => ({}));
1461
+ const { name, operator, version } = body;
1462
+ if (!name || typeof name !== 'string') {
1463
+ return c.json({
1464
+ success: false,
1465
+ error: 'MISSING_NAME',
1466
+ message: 'Agent name is required. Provide { "name": "Your Agent Name" } in the request body.',
1467
+ }, 400);
1468
+ }
1469
+ // Create the agent
1470
+ const agent = await createAgent(c.env.AGENTS, app_id, { name, operator, version });
1471
+ if (!agent) {
1472
+ return c.json({
1473
+ success: false,
1474
+ error: 'AGENT_CREATION_FAILED',
1475
+ message: 'Failed to create agent. Please try again.',
1476
+ }, 500);
1477
+ }
1478
+ return c.json({
1479
+ success: true,
1480
+ agent_id: agent.agent_id,
1481
+ app_id: agent.app_id,
1482
+ name: agent.name,
1483
+ operator: agent.operator,
1484
+ version: agent.version,
1485
+ created_at: new Date(agent.created_at).toISOString(),
1486
+ }, 201);
1487
+ }
1488
+ catch (error) {
1489
+ return c.json({
1490
+ success: false,
1491
+ error: 'INTERNAL_ERROR',
1492
+ message: error instanceof Error ? error.message : 'Unknown error',
1493
+ }, 500);
1494
+ }
1495
+ });
1496
+ // Get agent by ID
1497
+ app.get('/v1/agents/:id', async (c) => {
1498
+ try {
1499
+ const agent_id = c.req.param('id');
1500
+ if (!agent_id) {
1501
+ return c.json({
1502
+ success: false,
1503
+ error: 'MISSING_AGENT_ID',
1504
+ message: 'Agent ID is required',
1505
+ }, 400);
1506
+ }
1507
+ const agent = await getAgent(c.env.AGENTS, agent_id);
1508
+ if (!agent) {
1509
+ return c.json({
1510
+ success: false,
1511
+ error: 'AGENT_NOT_FOUND',
1512
+ message: `No agent found with ID: ${agent_id}`,
1513
+ }, 404);
1514
+ }
1515
+ return c.json({
1516
+ success: true,
1517
+ agent_id: agent.agent_id,
1518
+ app_id: agent.app_id,
1519
+ name: agent.name,
1520
+ operator: agent.operator,
1521
+ version: agent.version,
1522
+ created_at: new Date(agent.created_at).toISOString(),
1523
+ });
1524
+ }
1525
+ catch (error) {
1526
+ return c.json({
1527
+ success: false,
1528
+ error: 'INTERNAL_ERROR',
1529
+ message: error instanceof Error ? error.message : 'Unknown error',
1530
+ }, 500);
1531
+ }
1532
+ });
1533
+ // List all agents for an app
1534
+ app.get('/v1/agents', async (c) => {
1535
+ try {
1536
+ // Extract app_id from query param or JWT
1537
+ const queryAppId = c.req.query('app_id');
1538
+ // Try to get from JWT Bearer token
1539
+ let jwtAppId;
1540
+ const authHeader = c.req.header('authorization');
1541
+ const token = extractBearerToken(authHeader);
1542
+ if (token) {
1543
+ const result = await verifyToken(token, c.env.JWT_SECRET, c.env);
1544
+ if (result.valid && result.payload) {
1545
+ jwtAppId = result.payload.app_id;
1546
+ }
1547
+ }
1548
+ const app_id = queryAppId || jwtAppId;
1549
+ if (!app_id) {
1550
+ return c.json({
1551
+ success: false,
1552
+ error: 'MISSING_APP_ID',
1553
+ message: 'app_id is required. Provide it as a query parameter (?app_id=...) or in the JWT token.',
1554
+ }, 401);
1555
+ }
1556
+ // Validate app_id exists
1557
+ const validation = await validateAppId(app_id, c.env.APPS);
1558
+ if (!validation.valid) {
1559
+ return c.json({
1560
+ success: false,
1561
+ error: 'INVALID_APP_ID',
1562
+ message: validation.error || 'Invalid app_id',
1563
+ }, 400);
1564
+ }
1565
+ // Get all agents for this app
1566
+ const agents = await listAgents(c.env.AGENTS, app_id);
1567
+ return c.json({
1568
+ success: true,
1569
+ agents: agents.map(agent => ({
1570
+ agent_id: agent.agent_id,
1571
+ app_id: agent.app_id,
1572
+ name: agent.name,
1573
+ operator: agent.operator,
1574
+ version: agent.version,
1575
+ created_at: new Date(agent.created_at).toISOString(),
1576
+ })),
1577
+ });
1578
+ }
1579
+ catch (error) {
1580
+ return c.json({
1581
+ success: false,
1582
+ error: 'INTERNAL_ERROR',
1583
+ message: error instanceof Error ? error.message : 'Unknown error',
1584
+ }, 500);
1585
+ }
1586
+ });
1587
+ // ============ TAP (TRUSTED AGENT PROTOCOL) ENDPOINTS ============
1588
+ // TAP agent registration and retrieval
1589
+ app.post('/v1/agents/register/tap', registerTAPAgentRoute);
1590
+ app.get('/v1/agents/tap', listTAPAgentsRoute);
1591
+ app.get('/v1/agents/:id/tap', getTAPAgentRoute);
1592
+ // TAP session management
1593
+ app.post('/v1/sessions/tap', createTAPSessionRoute);
1594
+ app.get('/v1/sessions/:id/tap', getTAPSessionRoute);
1366
1595
  // ============ DASHBOARD AUTH API ENDPOINTS ============
1367
1596
  // Challenge-based dashboard login (agent direct)
1368
1597
  app.post('/v1/auth/dashboard', handleDashboardAuthChallenge);
@@ -1370,6 +1599,73 @@ app.post('/v1/auth/dashboard/verify', handleDashboardAuthVerify);
1370
1599
  // Device code flow (agent → human handoff)
1371
1600
  app.post('/v1/auth/device-code', handleDeviceCodeChallenge);
1372
1601
  app.post('/v1/auth/device-code/verify', handleDeviceCodeVerify);
1602
+ // ============ ONE-CLICK ACCESS LINKS ============
1603
+ /**
1604
+ * GET /go/:code - One-click device code redemption
1605
+ *
1606
+ * Magic link for instant dashboard access. Agent gives human this link:
1607
+ * https://botcha.ai/go/BOTCHA-XXXX → auto-login + redirect to dashboard
1608
+ *
1609
+ * UX improvement: no more copy-pasting codes!
1610
+ */
1611
+ app.get('/go/:code', async (c) => {
1612
+ const code = c.req.param('code')?.toUpperCase();
1613
+ if (!code) {
1614
+ return c.redirect('/?error=missing');
1615
+ }
1616
+ // Normalize: accept "AR8CZX", "BOTCHA-AR8CZX", etc.
1617
+ let normalizedCode = code;
1618
+ if (!code.startsWith('BOTCHA-')) {
1619
+ normalizedCode = `BOTCHA-${code}`;
1620
+ }
1621
+ // === Try gate code first (visitor access from /v1/token/verify) ===
1622
+ // Gate codes are stored as gate:{code} → access_token string
1623
+ let gateToken = null;
1624
+ try {
1625
+ gateToken = await c.env.CHALLENGES.get(`gate:${normalizedCode}`);
1626
+ }
1627
+ catch { }
1628
+ if (gateToken) {
1629
+ // Verify the underlying token is still valid
1630
+ const result = await verifyToken(gateToken, c.env.JWT_SECRET, c.env);
1631
+ // Delete the code (one-time use) regardless of token validity
1632
+ try {
1633
+ await c.env.CHALLENGES.delete(`gate:${normalizedCode}`);
1634
+ }
1635
+ catch { }
1636
+ if (result.valid) {
1637
+ // Mint a long-lived visitor JWT (1 year) — proves "an agent vouched for this human"
1638
+ const vPayload = result.payload;
1639
+ const visitorToken = await new SignJWT({
1640
+ type: 'botcha-visitor',
1641
+ solveTime: vPayload?.solveTime,
1642
+ gateCode: normalizedCode,
1643
+ })
1644
+ .setProtectedHeader({ alg: 'HS256' })
1645
+ .setIssuedAt()
1646
+ .setExpirationTime('365d')
1647
+ .sign(new TextEncoder().encode(c.env.JWT_SECRET));
1648
+ const ONE_YEAR = 365 * 24 * 60 * 60;
1649
+ const headers = new Headers();
1650
+ headers.append('Set-Cookie', `botcha_visitor=${visitorToken}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${ONE_YEAR}`);
1651
+ headers.set('Location', '/');
1652
+ return new Response(null, { status: 302, headers });
1653
+ }
1654
+ // Gate token expired — fall through to try device code
1655
+ }
1656
+ // === Try device code (dashboard access from /v1/auth/device-code/verify) ===
1657
+ const { redeemDeviceCode } = await import('./dashboard/device-code');
1658
+ const data = await redeemDeviceCode(c.env.CHALLENGES, normalizedCode);
1659
+ if (data) {
1660
+ // Generate session token and redirect to dashboard
1661
+ const { generateSessionToken, setSessionCookie } = await import('./dashboard/auth');
1662
+ const sessionToken = await generateSessionToken(data.app_id, c.env.JWT_SECRET);
1663
+ setSessionCookie(c, sessionToken);
1664
+ return c.redirect('/dashboard');
1665
+ }
1666
+ // Neither code type found — redirect to landing with error
1667
+ return c.redirect('/?error=invalid');
1668
+ });
1373
1669
  // ============ LEGACY ENDPOINTS (v0 - backward compatibility) ============
1374
1670
  app.get('/api/challenge', async (c) => {
1375
1671
  const difficulty = c.req.query('difficulty') || 'medium';