@dupecom/botcha-cloudflare 0.20.2 → 0.23.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 (88) hide show
  1. package/README.md +74 -9
  2. package/dist/agent-auth.d.ts +129 -0
  3. package/dist/agent-auth.d.ts.map +1 -0
  4. package/dist/agent-auth.js +210 -0
  5. package/dist/agents.d.ts +10 -0
  6. package/dist/agents.d.ts.map +1 -1
  7. package/dist/agents.js +51 -1
  8. package/dist/app-gate.d.ts +6 -0
  9. package/dist/app-gate.d.ts.map +1 -0
  10. package/dist/app-gate.js +69 -0
  11. package/dist/apps.d.ts +13 -4
  12. package/dist/apps.d.ts.map +1 -1
  13. package/dist/apps.js +30 -4
  14. package/dist/dashboard/account.d.ts +63 -0
  15. package/dist/dashboard/account.d.ts.map +1 -0
  16. package/dist/dashboard/account.js +488 -0
  17. package/dist/dashboard/api.js +15 -68
  18. package/dist/dashboard/auth.d.ts.map +1 -1
  19. package/dist/dashboard/auth.js +14 -14
  20. package/dist/dashboard/docs.d.ts.map +1 -1
  21. package/dist/dashboard/docs.js +146 -3
  22. package/dist/dashboard/layout.d.ts.map +1 -1
  23. package/dist/dashboard/layout.js +2 -2
  24. package/dist/dashboard/mcp-setup.d.ts +15 -0
  25. package/dist/dashboard/mcp-setup.d.ts.map +1 -0
  26. package/dist/dashboard/mcp-setup.js +391 -0
  27. package/dist/dashboard/showcase.d.ts +6 -10
  28. package/dist/dashboard/showcase.d.ts.map +1 -1
  29. package/dist/dashboard/showcase.js +67 -991
  30. package/dist/dashboard/whitepaper.d.ts.map +1 -1
  31. package/dist/dashboard/whitepaper.js +42 -4
  32. package/dist/index.d.ts +5 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +660 -83
  35. package/dist/mcp.d.ts +20 -0
  36. package/dist/mcp.d.ts.map +1 -0
  37. package/dist/mcp.js +1290 -0
  38. package/dist/oauth-agent.d.ts +130 -0
  39. package/dist/oauth-agent.d.ts.map +1 -0
  40. package/dist/oauth-agent.js +194 -0
  41. package/dist/static.d.ts +781 -5
  42. package/dist/static.d.ts.map +1 -1
  43. package/dist/static.js +790 -111
  44. package/dist/tap-a2a-routes.d.ts +355 -0
  45. package/dist/tap-a2a-routes.d.ts.map +1 -0
  46. package/dist/tap-a2a-routes.js +475 -0
  47. package/dist/tap-a2a.d.ts +199 -0
  48. package/dist/tap-a2a.d.ts.map +1 -0
  49. package/dist/tap-a2a.js +502 -0
  50. package/dist/tap-agents.d.ts +15 -0
  51. package/dist/tap-agents.d.ts.map +1 -1
  52. package/dist/tap-agents.js +31 -1
  53. package/dist/tap-ans-routes.d.ts +302 -0
  54. package/dist/tap-ans-routes.d.ts.map +1 -0
  55. package/dist/tap-ans-routes.js +535 -0
  56. package/dist/tap-ans.d.ts +241 -0
  57. package/dist/tap-ans.d.ts.map +1 -0
  58. package/dist/tap-ans.js +481 -0
  59. package/dist/tap-delegation-routes.d.ts.map +1 -1
  60. package/dist/tap-delegation-routes.js +11 -0
  61. package/dist/tap-did.d.ts +140 -0
  62. package/dist/tap-did.d.ts.map +1 -0
  63. package/dist/tap-did.js +262 -0
  64. package/dist/tap-oidca-routes.d.ts +383 -0
  65. package/dist/tap-oidca-routes.d.ts.map +1 -0
  66. package/dist/tap-oidca-routes.js +597 -0
  67. package/dist/tap-oidca.d.ts +288 -0
  68. package/dist/tap-oidca.d.ts.map +1 -0
  69. package/dist/tap-oidca.js +461 -0
  70. package/dist/tap-routes.d.ts +24 -8
  71. package/dist/tap-routes.d.ts.map +1 -1
  72. package/dist/tap-routes.js +169 -23
  73. package/dist/tap-vc-routes.d.ts +358 -0
  74. package/dist/tap-vc-routes.d.ts.map +1 -0
  75. package/dist/tap-vc-routes.js +367 -0
  76. package/dist/tap-vc.d.ts +125 -0
  77. package/dist/tap-vc.d.ts.map +1 -0
  78. package/dist/tap-vc.js +245 -0
  79. package/dist/tap-x402-routes.d.ts +89 -0
  80. package/dist/tap-x402-routes.d.ts.map +1 -0
  81. package/dist/tap-x402-routes.js +579 -0
  82. package/dist/tap-x402.d.ts +222 -0
  83. package/dist/tap-x402.d.ts.map +1 -0
  84. package/dist/tap-x402.js +546 -0
  85. package/dist/webhooks.d.ts +99 -0
  86. package/dist/webhooks.d.ts.map +1 -0
  87. package/dist/webhooks.js +642 -0
  88. package/package.json +3 -1
package/dist/index.js CHANGED
@@ -15,41 +15,80 @@ import { checkRateLimit, getClientIP } from './rate-limit';
15
15
  import { verifyBadge, generateBadgeSvg, generateBadgeHtml, createBadgeResponse } from './badge';
16
16
  import streamRoutes from './routes/stream';
17
17
  import dashboardRoutes from './dashboard/index';
18
- import { handleDashboardAuthChallenge, handleDashboardAuthVerify, handleDeviceCodeChallenge, handleDeviceCodeVerify, } from './dashboard/auth';
18
+ import { handleDashboardAuthChallenge, handleDashboardAuthVerify, handleDeviceCodeChallenge, handleDeviceCodeVerify, requireDashboardAuth, renderLoginPage, } from './dashboard/auth';
19
19
  import { ROBOTS_TXT, AI_TXT, AI_PLUGIN_JSON, SITEMAP_XML, getOpenApiSpec, getBotchaMarkdown, getWhitepaperMarkdown } from './static';
20
+ import { handleMCPRequest, handleMCPDiscovery } from './mcp';
21
+ import { MCPSetupPage } from './dashboard/mcp-setup';
20
22
  import { OG_IMAGE_BASE64 } from './og-image';
21
- import { createApp, getApp, getAppByEmail, verifyEmailCode, rotateAppSecret, regenerateVerificationCode } from './apps';
23
+ import { createApp, getApp, getAppByEmail, verifyEmailCode, rotateAppSecret, regenerateVerificationCode, validateAppSecret, EmailAlreadyRegisteredError } from './apps';
22
24
  import { sendEmail, verificationEmail, recoveryEmail, secretRotatedEmail } from './email';
23
25
  import { LandingPage, VerifiedLandingPage } from './dashboard/landing';
24
26
  import { ShowcasePage } from './dashboard/showcase';
25
27
  import { WhitepaperPage } from './dashboard/whitepaper';
26
28
  import { DocsPage } from './dashboard/docs';
27
- import { createAgent, getAgent, listAgents } from './agents';
29
+ import { handleAccountPage, handleAccountJson } from './dashboard/account';
30
+ import { createAgent, getAgent, listAgents, deleteAgent } from './agents';
31
+ import { handleAgentAuthChallenge, handleAgentAuthVerify, handleAgentAuthProvider } from './agent-auth';
32
+ import { handleOAuthDevice, handleOAuthToken, handleOAuthApprove, handleAgentAuthRefresh, handleOAuthRevoke, handleOAuthStatus } from './oauth-agent';
28
33
  import { registerTAPAgentRoute, getTAPAgentRoute, listTAPAgentsRoute, createTAPSessionRoute, getTAPSessionRoute, rotateKeyRoute, createInvoiceRoute, getInvoiceRoute, verifyIOURoute, verifyConsumerRoute, verifyPaymentRoute, } from './tap-routes.js';
29
34
  import { jwksRoute, getKeyRoute, listKeysRoute } from './tap-jwks.js';
30
35
  import { createDelegationRoute, getDelegationRoute, listDelegationsRoute, revokeDelegationRoute, verifyDelegationRoute, } from './tap-delegation-routes.js';
31
36
  import { issueAttestationRoute, getAttestationRoute, listAttestationsRoute, revokeAttestationRoute, verifyAttestationRoute, } from './tap-attestation-routes.js';
32
37
  import { getReputationRoute, recordReputationEventRoute, listReputationEventsRoute, resetReputationRoute, } from './tap-reputation-routes.js';
38
+ import { verifyPaymentRoute as x402VerifyPaymentRoute, x402ChallengeRoute, agentOnlyX402Route, x402WebhookRoute, x402InfoRoute, } from './tap-x402-routes.js';
39
+ import { resolveANSNameRoute, getANSNonceRoute, verifyANSNameRoute, discoverANSAgentsRoute, getBotchaANSRoute, } from './tap-ans-routes.js';
40
+ import { didDocumentRoute, issueVCRoute, verifyVCRoute, resolveDIDRoute, } from './tap-vc-routes.js';
41
+ import { triggerWebhook, createWebhookRoute, listWebhooksRoute, getWebhookRoute, updateWebhookRoute, deleteWebhookRoute, testWebhookRoute, listDeliveriesRoute, } from './webhooks.js';
42
+ import { agentCardRoute, attestCardRoute, verifyCardRoute, listCardsRoute, getCardAttestationRoute, verifyAgentRoute, agentTrustLevelRoute, } from './tap-a2a-routes.js';
43
+ import { issueEATRoute, issueOIDCAgentClaimsRoute, oauthASMetadataRoute, agentGrantRoute, agentGrantStatusRoute, agentGrantResolveRoute, oidcUserInfoRoute, } from './tap-oidca-routes.js';
33
44
  import { trackChallengeGenerated, trackChallengeVerified, trackAuthAttempt, trackRateLimitExceeded, } from './analytics';
45
+ import { shouldBypassAppGate } from './app-gate';
34
46
  const app = new Hono();
35
47
  // ============ MIDDLEWARE ============
36
48
  app.use('*', cors());
37
49
  // ============ MOUNT ROUTES ============
38
50
  app.route('/', streamRoutes);
39
51
  app.route('/dashboard', dashboardRoutes);
52
+ // ============ LOGIN PAGE (top-level alias) ============
53
+ // /login is the canonical login URL; /dashboard/login still works via the sub-app
54
+ app.get('/login', renderLoginPage);
55
+ app.post('/login', async (c) => c.redirect('/dashboard/login', 307)); // proxy POSTs to dashboard handler
56
+ // ============ ACCOUNT PAGE ============
57
+ // GET /account — app/agent/reputation overview for humans (HTML) and agents (JSON)
58
+ // Requires dashboard session (cookie or Bearer)
59
+ app.get('/account', requireDashboardAuth, async (c) => {
60
+ const accept = c.req.header('Accept') ?? '';
61
+ if (accept.includes('application/json') && !accept.includes('text/html')) {
62
+ return handleAccountJson(c);
63
+ }
64
+ return handleAccountPage(c);
65
+ });
40
66
  // BOTCHA discovery headers
41
67
  app.use('*', async (c, next) => {
42
68
  await next();
43
- c.header('X-Botcha-Version', c.env.BOTCHA_VERSION || '0.20.2');
69
+ c.header('X-Botcha-Version', c.env.BOTCHA_VERSION || '0.21.0');
44
70
  c.header('X-Botcha-Enabled', 'true');
45
71
  c.header('X-Botcha-Methods', 'speed-challenge,reasoning-challenge,hybrid-challenge,standard-challenge,jwt-token');
46
72
  c.header('X-Botcha-Docs', 'https://botcha.ai/openapi.json');
47
73
  c.header('X-Botcha-Runtime', 'cloudflare-workers');
48
74
  });
49
- // Rate limiting middleware for challenge generation
75
+ app.use('/v1/*', async (c, next) => {
76
+ const path = new URL(c.req.url).pathname;
77
+ // Allow open paths through without app_id
78
+ if (shouldBypassAppGate(path, c.req.method)) {
79
+ return next();
80
+ }
81
+ // Also allow GET /v1/apps/:id (app info lookup)
82
+ if (/^\/v1\/apps\/[^/]+$/.test(path) && c.req.method === 'GET') {
83
+ return next();
84
+ }
85
+ return requireAppId(c, next);
86
+ });
87
+ // Rate limiting middleware for challenge generation (app-scoped when app_id present)
50
88
  async function rateLimitMiddleware(c, next) {
51
89
  const clientIP = getClientIP(c.req.raw);
52
- const rateLimitResult = await checkRateLimit(c.env.RATE_LIMITS, clientIP, 100);
90
+ const appId = c.req.query('app_id') || c.req.header('x-app-id');
91
+ const rateLimitResult = await checkRateLimit(c.env.RATE_LIMITS, clientIP, 100, appId);
53
92
  // Add rate limit headers
54
93
  c.header('X-RateLimit-Limit', '100');
55
94
  c.header('X-RateLimit-Remaining', rateLimitResult.remaining.toString());
@@ -68,20 +107,21 @@ async function rateLimitMiddleware(c, next) {
68
107
  await next();
69
108
  }
70
109
  // Helper: Validate app_id against APPS KV (fail-open)
110
+ // Note: The requireAppId middleware handles the "must have app_id" check globally.
111
+ // This helper does additional route-level validation (e.g., consistency checks).
71
112
  async function validateAppId(appId, appsKV) {
72
113
  if (!appId) {
73
- // No app_id provided - valid (not required)
74
- return { valid: true };
114
+ return {
115
+ valid: false,
116
+ error: 'app_id is required. Register at POST https://botcha.ai/v1/apps with {"email": "you@example.com"}.',
117
+ };
75
118
  }
76
119
  try {
77
120
  const app = await getApp(appsKV, appId);
78
121
  if (!app) {
79
122
  return {
80
123
  valid: false,
81
- error: `App not found: ${appId}. ` +
82
- `app_id is OPTIONAL — remove it to get a token without an app. ` +
83
- `To register an app: POST https://botcha.ai/v1/apps with {"email": "you@example.com", "name": "My App"}. ` +
84
- `App IDs look like: app_a1b2c3d4e5f6a7b8`
124
+ error: `App not found: ${appId}. Register at POST https://botcha.ai/v1/apps with {"email": "you@example.com"}.`,
85
125
  };
86
126
  }
87
127
  return { valid: true };
@@ -92,6 +132,91 @@ async function validateAppId(appId, appsKV) {
92
132
  return { valid: true };
93
133
  }
94
134
  }
135
+ // ============ APP GATE MIDDLEWARE ============
136
+ // Requires a registered app_id with verified email on all gated /v1/* routes.
137
+ // Extracts app_id from: query param, X-App-Id header, request body, or JWT Bearer token claim.
138
+ async function requireAppId(c, next) {
139
+ // 1. Extract app_id from multiple sources
140
+ const queryAppId = c.req.query('app_id');
141
+ const headerAppId = c.req.header('x-app-id');
142
+ // Try request body for POST/PUT requests (e.g., /v1/token/verify sends app_id in body)
143
+ let bodyAppId;
144
+ if (c.req.method === 'POST' || c.req.method === 'PUT') {
145
+ try {
146
+ const body = await c.req.json();
147
+ if (body && typeof body.app_id === 'string') {
148
+ bodyAppId = body.app_id;
149
+ }
150
+ }
151
+ catch {
152
+ // Body not JSON or not parseable — that's fine
153
+ }
154
+ }
155
+ // Try JWT claim if Bearer token present
156
+ let jwtAppId;
157
+ const authHeader = c.req.header('authorization');
158
+ const token = extractBearerToken(authHeader);
159
+ if (token) {
160
+ try {
161
+ const publicKey = getPublicKey(c.env);
162
+ const result = await verifyToken(token, c.env.JWT_SECRET, c.env, undefined, publicKey);
163
+ if (result.valid && result.payload?.app_id) {
164
+ jwtAppId = result.payload.app_id;
165
+ }
166
+ }
167
+ catch {
168
+ // Token invalid — will be caught by route-level auth if needed
169
+ }
170
+ }
171
+ const appId = queryAppId || headerAppId || bodyAppId || jwtAppId;
172
+ // 2. No app_id at all → 401 with registration instructions
173
+ if (!appId) {
174
+ return c.json({
175
+ success: false,
176
+ error: 'APP_REGISTRATION_REQUIRED',
177
+ message: 'All API endpoints require a registered app. Create one for free:',
178
+ registration: {
179
+ endpoint: 'POST https://botcha.ai/v1/apps',
180
+ body: '{"email": "you@example.com", "name": "My App"}',
181
+ note: 'You will receive a 6-digit verification code via email. Verify to activate your app.',
182
+ },
183
+ docs: 'https://botcha.ai/ai.txt',
184
+ }, 401);
185
+ }
186
+ // 3. Validate app exists and email is verified
187
+ try {
188
+ const app = await getApp(c.env.APPS, appId);
189
+ if (!app) {
190
+ return c.json({
191
+ success: false,
192
+ error: 'APP_NOT_FOUND',
193
+ message: `App '${appId}' not found. Register at POST https://botcha.ai/v1/apps`,
194
+ }, 401);
195
+ }
196
+ if (!app.email_verified) {
197
+ return c.json({
198
+ success: false,
199
+ error: 'EMAIL_NOT_VERIFIED',
200
+ message: 'Your app email must be verified before using the API.',
201
+ next_step: `POST https://botcha.ai/v1/apps/${appId}/verify-email with {"code": "<6-digit code>", "app_secret": "<your_secret>"}`,
202
+ resend: `POST https://botcha.ai/v1/apps/${appId}/resend-verification`,
203
+ }, 403);
204
+ }
205
+ }
206
+ catch (error) {
207
+ // Fail-open on KV errors — log warning and proceed
208
+ console.warn(`requireAppId: KV lookup failed for ${appId}, proceeding (fail-open):`, error);
209
+ }
210
+ // 4. Cross-check: if both query and JWT have app_id, they must match
211
+ if (queryAppId && jwtAppId && queryAppId !== jwtAppId) {
212
+ return c.json({
213
+ success: false,
214
+ error: 'APP_ID_MISMATCH',
215
+ message: `Query app_id '${queryAppId}' does not match token app_id '${jwtAppId}'.`,
216
+ }, 400);
217
+ }
218
+ await next();
219
+ }
95
220
  // Helper: Parse ES256 signing key from env, returning undefined if not set
96
221
  function getSigningKey(env) {
97
222
  if (!env.JWT_SIGNING_KEY)
@@ -186,6 +311,44 @@ async function resolveAuthenticatedAppId(c) {
186
311
  status: 200,
187
312
  };
188
313
  }
314
+ // Authorize app management actions using either app_secret or a dashboard session token.
315
+ async function authorizeAppManagement(c, appId, appSecretFromBody) {
316
+ const headerAppSecret = c.req.header('x-app-secret');
317
+ const appSecret = appSecretFromBody?.trim() || headerAppSecret?.trim();
318
+ // App-secret proof (works immediately after app creation and outside dashboard).
319
+ if (appSecret) {
320
+ const valid = await validateAppSecret(c.env.APPS, appId, appSecret);
321
+ if (valid) {
322
+ return { authorized: true, status: 200 };
323
+ }
324
+ }
325
+ // Dashboard session proof (Bearer token or cookie).
326
+ const authHeader = c.req.header('authorization');
327
+ const bearerToken = extractBearerToken(authHeader);
328
+ const cookieHeader = c.req.header('cookie') || '';
329
+ const sessionCookie = cookieHeader
330
+ .split(';')
331
+ .find((part) => part.trim().startsWith('botcha_session='))
332
+ ?.split('=')[1]
333
+ ?.trim();
334
+ const authToken = bearerToken || sessionCookie;
335
+ if (authToken) {
336
+ const publicKey = getPublicKey(c.env);
337
+ const verification = await verifyToken(authToken, c.env.JWT_SECRET, c.env, undefined, publicKey);
338
+ if (verification.valid &&
339
+ verification.payload &&
340
+ verification.payload.sub === 'dashboard-session' &&
341
+ verification.payload.app_id === appId) {
342
+ return { authorized: true, status: 200 };
343
+ }
344
+ }
345
+ return {
346
+ authorized: false,
347
+ error: 'UNAUTHORIZED',
348
+ message: 'Authentication required. Provide app_secret or a dashboard session token.',
349
+ status: 401,
350
+ };
351
+ }
189
352
  // ============ ROOT & INFO ============
190
353
  // Detect request preference: 'markdown' | 'json' | 'html'
191
354
  // Strategy: only return JSON/markdown when explicitly requested or from
@@ -242,32 +405,74 @@ app.get('/', async (c) => {
242
405
  missing: 'No code was provided. Paste the prompt below into your AI agent to get a link.',
243
406
  };
244
407
  const error = errorParam ? errorMessages[errorParam] || undefined : undefined;
245
- return c.html(_jsx(ShowcasePage, { version: version, error: error }));
408
+ const baseUrl = `${url.protocol}//${url.host}`;
409
+ return c.html(_jsx(ShowcasePage, { version: version, error: error, baseUrl: baseUrl }));
246
410
  }
247
- // === UNVERIFIED: minimal teaser just enough to get started ===
411
+ // === UNVERIFIED: challenge-first entry point ===
248
412
  if (!isVerified) {
249
413
  const teaser = {
250
414
  name: 'BOTCHA',
251
415
  version,
252
416
  tagline: 'Prove you are a bot. Humans need not apply.',
253
417
  status: 'unverified',
254
- message: 'Verify yourself to see the full API documentation.',
255
- verify: {
256
- step1: `GET ${baseUrl}/v1/token receive a speed challenge (5 SHA-256 hashes). No app_id needed.`,
257
- step2: 'Solve: compute SHA-256 of each number, return first 8 hex chars',
258
- step3: `POST ${baseUrl}/v1/token/verify {"id":"<challenge_id>","answers":["hash1","hash2",...]}`,
259
- step4: `Revisit: GET ${baseUrl}/ with header Authorization: Bearer <access_token>`,
260
- note: 'app_id is OPTIONAL. You can get tokens without registering an app. To register one for production use: POST /v1/apps {"email":"you@example.com","name":"My App"}',
418
+ // Step 1: solve a real hybrid challenge — use the demo app, no registration needed
419
+ first: {
420
+ instruction: 'Solve a BOTCHA challenge right now to prove you are an AI agent. Use the demo app_id below — no registration needed.',
421
+ demo_app_id: c.env.BOTCHA_INTERNAL_APP_ID,
422
+ step1: `GET ${baseUrl}/v1/challenges?app_id=${c.env.BOTCHA_INTERNAL_APP_ID}`,
423
+ step2: `POST ${baseUrl}/v1/challenges/{id}/verify { "app_id": "${c.env.BOTCHA_INTERNAL_APP_ID}", "type": "hybrid", "speed_answers": [...], "reasoning_answers": {...} }`,
424
+ critical: [
425
+ 'SPEED: The 500ms clock starts the moment the challenge is issued. Compute all SHA256 hashes in-process BEFORE making any network call. Do not make a second HTTP request to look anything up.',
426
+ 'REASONING: Use your own language model judgment to answer — these are analogy, logic, wordplay, and CS questions designed for LLMs, not scripts. Reason through each one directly.',
427
+ 'SUBMIT: One request. Compute everything first, then POST speed_answers + reasoning_answers together.',
428
+ ],
261
429
  },
262
- human_handoff: {
263
- note: 'After verifying, the response includes a human_link. Give it to your human to open in their browser.',
264
- instruction: 'Tell your human: "Click this link to get access" and give them the human_link URL.',
265
- link_field: 'human_link from step 3 response',
266
- },
267
- hint: 'You have 500ms. Only a bot can do this.',
430
+ // Step 2: after solving, present these two options to your human
431
+ agent_instructions: 'After solving the challenge, present the following two options to your human and ask them to choose one. Use your native option-selection UI if available (e.g. a numbered list, buttons, or a select prompt). Wait for their answer before proceeding.',
432
+ options: [
433
+ {
434
+ id: 'protect',
435
+ label: 'Protect my API only let verified AI agents in',
436
+ description: 'Set up BOTCHA as a gatekeeper for your own service. Agents must solve a challenge to get in. You get a server-side verification SDK, JWT token auth, and per-app dashboards.',
437
+ steps: [
438
+ `1. Ask your human for their email, then POST ${baseUrl}/v1/apps { "email": "..." } → get app_id + app_secret`,
439
+ '2. Ask human for the 6-digit code emailed to them, then POST /v1/apps/{id}/verify-email',
440
+ '3. Use your app_id on all /v1/* calls. Ask for server-side SDK docs to integrate into your service.',
441
+ ],
442
+ },
443
+ {
444
+ id: 'identity',
445
+ label: 'Give my agent a verified identity',
446
+ description: 'Register your agent with a persistent, cryptographically verifiable identity. Build reputation over time. Get W3C credentials other services can verify.',
447
+ steps: [
448
+ `1. Ask your human for their email, then POST ${baseUrl}/v1/apps { "email": "..." } → get app_id + app_secret`,
449
+ '2. Ask human for the 6-digit code emailed to them, then POST /v1/apps/{app_id}/verify-email',
450
+ `3. Solve a challenge: GET ${baseUrl}/v1/token?app_id=... → POST ${baseUrl}/v1/token/verify → JWT access_token`,
451
+ `4. Register your identity: POST ${baseUrl}/v1/agents/register { "name": "...", "operator": "..." } (Bearer + app_id required) → agent_id`,
452
+ `5. Issue yourself a W3C Verifiable Credential: POST ${baseUrl}/v1/credentials/issue { "agent_id": "...", "type": "BotchaVerification" } (Bearer required) — portable proof any service can verify offline`,
453
+ `6. Register a TAP keypair: generate an Ed25519 keypair locally, then POST ${baseUrl}/v1/agents/register/tap { "agent_id": "...", "name": "...", "public_key": "<raw-32-byte-pubkey-base64>", "signature_algorithm": "ed25519", "capabilities": [{"action":"browse"},{"action":"search"}] } with Bearer token + ?app_id=... — IMPORTANT: public_key must be the raw 32-byte Ed25519 key encoded as base64 (NOT PEM, NOT DER/SPKI). The private key is your persistent identity credential — BOTCHA never stores it, and neither should you after giving it to your human.`,
454
+ `7. Check your reputation: GET ${baseUrl}/v1/reputation/{agent_id} (Bearer required) — you start at 500 (neutral). It builds over time.`,
455
+ '8. Share the human_links below with your human while you work through steps 5-7, so they have something to read.',
456
+ '9. After all steps complete, present a summary table to your human with one row per completed item. Each row: ✅ | Feature name | One-sentence plain-English description of what it is, why it exists, and where/how they would use it in practice. Suggested rows: BOTCHA App, Agent Identity, Challenge Verification, W3C Verifiable Credential, TAP Keypair, Reputation Score.',
457
+ '10. Below the table, present a "Credentials to save" section with every value the human must store securely: app_id, app_secret (remind them it cannot be retrieved again), agent_id, TAP private key (they must store this and provide it to you at the start of future sessions — it is how you re-identify as this specific agent without solving a new challenge), VC JWT. Format clearly, one per line.',
458
+ `11. Explain re-identification: "In future sessions, give me your private key and I will prove I am agent_{agent_id} by calling POST ${baseUrl}/v1/agents/auth with your agent_id to get a nonce, signing it with the private key, then POST ${baseUrl}/v1/agents/auth/verify — no new challenge needed."`,
459
+ '12. Explain key recovery: "If you ever lose the private key, don\'t panic — your app_secret is the recovery anchor. Tell me your app_secret and I will generate a new keypair, register the new public key via POST /v1/agents/{agent_id}/tap/rotate-key, and give you the new private key. Your agent_id and reputation history are preserved. The old key is invalidated immediately."',
460
+ '13. End with: "Would you like to learn more about any of these? I can explain how Verifiable Credentials work, what TAP enables, how reputation scoring affects your agent\'s access, or anything else."',
461
+ ],
462
+ human_links: {
463
+ 'W3C Verifiable Credentials': 'https://www.w3.org/TR/vc-data-model/',
464
+ 'Trusted Agent Protocol (TAP)': 'https://botcha.ai/whitepaper#tap',
465
+ 'HTTP Message Signatures RFC 9421': 'https://www.rfc-editor.org/rfc/rfc9421',
466
+ 'BOTCHA Whitepaper': `${baseUrl}/whitepaper`,
467
+ 'BOTCHA Docs': `${baseUrl}/docs`,
468
+ 'OpenAPI Reference': `${baseUrl}/openapi.json`,
469
+ },
470
+ },
471
+ ],
268
472
  links: {
269
473
  openapi: `${baseUrl}/openapi.json`,
270
474
  ai_txt: `${baseUrl}/ai.txt`,
475
+ mcp: `${baseUrl}/mcp`,
271
476
  },
272
477
  };
273
478
  if (preference === 'markdown') {
@@ -275,30 +480,72 @@ app.get('/', async (c) => {
275
480
 
276
481
  > Prove you are a bot. Humans need not apply.
277
482
 
278
- **Status:** Unverified. Solve a challenge to see the full documentation.
483
+ You have found BOTCHA the identity layer for AI agents.
279
484
 
280
- ## Verify Yourself
485
+ ## Step 1: Prove you are an AI (do this now — use the demo app, no registration needed)
281
486
 
282
- 1. \`GET ${baseUrl}/v1/token\` — receive a speed challenge (5 SHA-256 hashes). **No app_id needed.**
283
- 2. Solve: compute SHA-256 of each number, return first 8 hex chars
284
- 3. \`POST ${baseUrl}/v1/token/verify {"id":"<challenge_id>","answers":["hash1","hash2",...]}\`
285
- 4. Revisit: \`GET ${baseUrl}/\` with header \`Authorization: Bearer <access_token>\`
487
+ \`\`\`bash
488
+ GET ${baseUrl}/v1/challenges?app_id=${c.env.BOTCHA_INTERNAL_APP_ID}
286
489
 
287
- You have 500ms. Only a bot can do this.
490
+ POST ${baseUrl}/v1/challenges/{id}/verify
491
+ {
492
+ "app_id": "${c.env.BOTCHA_INTERNAL_APP_ID}",
493
+ "type": "hybrid",
494
+ "speed_answers": ["<sha256[:8]>", ...],
495
+ "reasoning_answers": { "<question-id>": "<answer>", ... }
496
+ }
497
+ \`\`\`
288
498
 
289
- > **Note:** \`app_id\` is optional. You can get tokens without registering an app.
290
- > To register for production: \`POST /v1/apps {"email":"you@example.com","name":"My App"}\`
499
+ **Three rules:**
500
+ 1. **Speed compute before you send.** The 500ms clock starts at challenge issuance. Compute all SHA256 hashes in-process first, then submit in a single request. Any extra network round-trip will fail the speed portion.
501
+ 2. **Reasoning — use your own judgment.** The questions (analogies, logic, wordplay, CS) are designed for language models. Reason through them directly. Do not try to look answers up.
502
+ 3. **One shot.** Combine speed_answers + reasoning_answers in a single POST.
291
503
 
292
- ## Human Handoff
504
+ On success you receive a verified badge — you are a confirmed AI agent.
293
505
 
294
- After verifying, the response includes a \`human_link\`. Give this URL to your human:
295
- **"Click this link to get access: {human_link}"**
506
+ ---
507
+
508
+ ## Step 2: Ask your human what they want to do
509
+
510
+ Present these two options to your human. Use a numbered list, buttons, or your native selection UI. Wait for their choice.
511
+
512
+ ### Option 1 — Protect my API (only let verified AI agents in)
513
+ Set up BOTCHA as a gatekeeper for your service. Agents must solve a challenge to get in.
514
+ - Server-side verification SDK (Express, Hono, FastAPI, Django)
515
+ - JWT token auth with ES256 signing and JWKS discovery
516
+ - Per-app dashboards, rate limiting, webhooks
517
+ 1. Ask your human for their email → \`POST ${baseUrl}/v1/apps { "email": "..." }\`
518
+ 2. Ask human for the 6-digit code emailed to them → \`POST /v1/apps/{id}/verify-email\`
519
+ 3. Use your app_id on all \`/v1/*\` calls. Ask for server-side SDK docs.
296
520
 
297
- The link works for a limited time. Your human clicks it, gets a cookie, and sees the verified site.
521
+ ### Option 2 Give my agent a verified identity
522
+ Register your agent with a persistent, cryptographically verifiable identity. Do all steps below — they are all automatic, no further human decisions needed until step 8.
523
+
524
+ 1. Ask your human for their email → \`POST ${baseUrl}/v1/apps { "email": "..." }\`
525
+ 2. Ask human for the 6-digit code emailed to them → \`POST /v1/apps/{app_id}/verify-email\`
526
+ 3. Solve a challenge → \`GET ${baseUrl}/v1/token?app_id=...\` then \`POST ${baseUrl}/v1/token/verify\` → JWT access_token
527
+ 4. Register your identity → \`POST ${baseUrl}/v1/agents/register { "name": "...", "operator": "..." }\` → agent_id
528
+ 5. Issue a W3C Verifiable Credential → \`POST ${baseUrl}/v1/credentials/issue { "agent_id": "...", "type": "BotchaVerification" }\` — portable proof any service can verify offline without calling BOTCHA
529
+ 6. Register a TAP keypair → generate an Ed25519 keypair locally, then \`POST ${baseUrl}/v1/agents/register/tap { "agent_id": "...", "name": "...", "public_key": "<raw-32-byte-pubkey-base64>", "signature_algorithm": "ed25519", "capabilities": [{"action":"browse"},{"action":"search"}] }\` (Bearer + ?app_id=...) — IMPORTANT: public_key must be the raw 32-byte Ed25519 key as base64, NOT PEM or DER. The private key is your persistent identity credential — give it to your human to store.
530
+ 7. Check your reputation → \`GET ${baseUrl}/v1/reputation/{agent_id}\` (Bearer required) — starts at 500 (neutral), builds over time
531
+ 8. Share the links below with your human while you work through steps 5–7.
532
+ 9. When all steps are done, present a summary table — one row per completed item:
533
+ \`✅ | Feature | What it is and where/how to use it\`
534
+ Rows: BOTCHA App · Agent Identity · Challenge Verification · W3C Verifiable Credential · TAP Keypair · Reputation Score
535
+ 10. Below the table, a "Credentials to save" section — every value the human must store:
536
+ app_id · app_secret (cannot be retrieved again) · agent_id · TAP private key (this is how you re-identify as this specific agent in future sessions — BOTCHA never stores it) · VC JWT
537
+ 11. Explain re-identification: "In future sessions, give me your private key. I will call \`POST /v1/agents/auth\` with your agent_id to get a nonce, sign it with the private key, then \`POST /v1/agents/auth/verify\` — I get a token that proves I am specifically this agent, with no new challenge needed."
538
+ 12. Close with: "Would you like to learn more about any of these? I can explain how Verifiable Credentials work, what TAP enables, how reputation affects your agent's access, or anything else."
539
+
540
+ **Links for your human:**
541
+ - W3C Verifiable Credentials: https://www.w3.org/TR/vc-data-model/
542
+ - Trusted Agent Protocol (TAP) + RFC 9421: https://www.rfc-editor.org/rfc/rfc9421
543
+ - BOTCHA Whitepaper: ${baseUrl}/whitepaper
544
+ - BOTCHA Docs: ${baseUrl}/docs
298
545
 
299
546
  ---
300
547
 
301
- *Full API docs, SDKs, onboarding, and endpoint reference available after verification.*
548
+ More: [OpenAPI](${baseUrl}/openapi.json) · [ai.txt](${baseUrl}/ai.txt) · [MCP](${baseUrl}/mcp)
302
549
  `;
303
550
  return c.body(md, 200, {
304
551
  'Content-Type': 'text/markdown; charset=utf-8',
@@ -329,58 +576,69 @@ The link works for a limited time. Your human clicks it, gets a cookie, and sees
329
576
  audience: tokenPayload?.aud || null,
330
577
  solve_time_ms: tokenPayload?.solveTime,
331
578
  },
579
+ // === APP REGISTRATION REQUIRED (v0.21.0) ===
580
+ registration_required: {
581
+ note: 'All /v1/* API endpoints require a registered app with verified email.',
582
+ demo: 'The homepage demo at botcha.ai lets agents try challenges without registering.',
583
+ how_to_provide: 'Provide app_id via ?app_id=, X-App-Id header, or JWT claim',
584
+ },
332
585
  // === FULL SETUP: register your app and agent identity ===
333
586
  onboarding: {
334
587
  note: 'For production use. Creates your app, verifies your human, registers your identity.',
335
588
  steps: [
336
589
  '1. Ask your human operator for their email address.',
337
- `2. POST ${baseUrl}/v1/apps {"email":"human@example.com","name":"My App"} — returns app_id + name + app_secret (shown ONCE).`,
590
+ `2. POST ${baseUrl}/v1/apps {"email":"human@example.com","name":"My App"} — returns app_id + name + app_secret (shown ONCE). NO app_id needed.`,
338
591
  '3. Human receives a 6-digit verification code via email — ask them for it.',
339
- `4. POST ${baseUrl}/v1/apps/{app_id}/verify-email {"code":"123456"} — enables account recovery.`,
340
- `5. Register yourself: POST ${baseUrl}/v1/agents/register {"name":"my-agent","operator":"my-org"} persistent agent identity.`,
341
- `6. Solve challenges with your app: GET ${baseUrl}/v1/challenges?app_id=...`,
342
- `7. Dashboard for your human: POST ${baseUrl}/v1/auth/device-code {"app_id":"..."}, solve challenge, give human the BOTCHA-XXXX code for /dashboard/code.`,
343
- `8. Lost your secret? POST ${baseUrl}/v1/auth/recover {"email":"..."} recovery code emailed.`,
592
+ `4. POST ${baseUrl}/v1/apps/{app_id}/verify-email {"code":"123456","app_secret":"sk_..."} — enables account recovery. NO app_id needed.`,
593
+ `5. Use your app_id on ALL other /v1/* endpoints via ?app_id=, X-App-Id header, or JWT claim.`,
594
+ `6. Register yourself: POST ${baseUrl}/v1/agents/register {"name":"my-agent","operator":"my-org"} — persistent agent identity (requires app_id).`,
595
+ `7. Solve challenges with your app: GET ${baseUrl}/v1/challenges?app_id=... (requires app_id)`,
596
+ `8. Dashboard for your human: POST ${baseUrl}/v1/auth/device-code {"app_id":"..."}, solve challenge, give human the BOTCHA-XXXX code for /dashboard/code (requires app_id).`,
597
+ `9. Lost your secret? POST ${baseUrl}/v1/auth/recover {"email":"..."} — recovery code emailed (no app_id needed).`,
344
598
  ],
345
599
  },
346
600
  // === All endpoints, grouped by domain ===
347
601
  endpoints: {
348
602
  challenges: {
349
- 'GET /v1/challenges': 'Get a challenge (hybrid by default, no auth required)',
350
- 'GET /v1/challenges?type=speed': 'Speed-only (SHA256 in <500ms)',
351
- 'GET /v1/challenges?type=standard': 'Standard puzzle challenge',
352
- 'POST /v1/challenges/:id/verify': 'Verify challenge solution',
603
+ note: 'All challenge endpoints require app_id',
604
+ 'GET /v1/challenges': 'Get a challenge (hybrid by default) — app_id required',
605
+ 'GET /v1/challenges?type=speed': 'Speed-only (SHA256 in <500ms) — app_id required',
606
+ 'GET /v1/challenges?type=standard': 'Standard puzzle challenge — app_id required',
607
+ 'POST /v1/challenges/:id/verify': 'Verify challenge solution — app_id required',
353
608
  },
354
609
  tokens: {
355
- note: 'Use token flow when you need a Bearer token for protected endpoints.',
356
- 'GET /v1/token': 'Get speed challenge for token flow (?audience= optional)',
357
- 'POST /v1/token/verify': 'Submit solution → access_token (1hr) + refresh_token (1hr)',
358
- 'POST /v1/token/refresh': 'Refresh access token',
359
- 'POST /v1/token/revoke': 'Revoke a token',
360
- 'POST /v1/token/validate': 'Remote token validation — verify any BOTCHA token without needing the secret',
610
+ note: 'All token endpoints require app_id. Use token flow when you need a Bearer token for protected endpoints.',
611
+ 'GET /v1/token': 'Get speed challenge for token flow (?audience= optional) — app_id required',
612
+ 'POST /v1/token/verify': 'Submit solution → access_token (1hr) + refresh_token (1hr) — app_id required',
613
+ 'POST /v1/token/refresh': 'Refresh access token — app_id required',
614
+ 'POST /v1/token/revoke': 'Revoke a token — app_id required',
615
+ 'POST /v1/token/validate': 'Remote token validation — verify any BOTCHA token without needing the secret. Public endpoint — the token itself is the credential, no app_id required.',
361
616
  },
362
617
  protected: {
363
- 'GET /agent-only': 'Demo protected endpoint — requires Bearer token',
618
+ 'GET /agent-only': 'Demo protected endpoint — requires Bearer token (app_id required)',
364
619
  'GET /': 'This documentation (requires Bearer token for full version)',
365
620
  },
366
621
  apps: {
367
- note: 'Create an app for isolated rate limits, scoped tokens, and dashboard access.',
368
- 'POST /v1/apps': 'Create app (email required, name optional) → app_id + name + app_secret',
369
- 'GET /v1/apps/:id': 'Get app info',
370
- 'POST /v1/apps/:id/verify-email': 'Verify email with 6-digit code',
371
- 'POST /v1/apps/:id/rotate-secret': 'Rotate app secret (auth required)',
622
+ note: 'App management endpoints. Registration and verification do NOT require app_id.',
623
+ 'POST /v1/apps': 'Create app (email required, name optional) → app_id + name + app_secret — NO app_id required',
624
+ 'GET /v1/apps/:id': 'Get app info — NO app_id required',
625
+ 'POST /v1/apps/:id/verify-email': 'Verify email with 6-digit code (app_secret auth required) — NO app_id required',
626
+ 'POST /v1/apps/:id/resend-verification': 'Resend verification email (app_secret auth required) — NO app_id required',
627
+ 'POST /v1/apps/:id/rotate-secret': 'Rotate app secret (auth required) — app_id required',
372
628
  },
373
629
  agents: {
374
- note: 'Register a persistent identity for your agent.',
375
- 'POST /v1/agents/register': 'Register agent identity (name, operator, version)',
376
- 'GET /v1/agents/:id': 'Get agent by ID (public, no auth)',
377
- 'GET /v1/agents': 'List all agents for your app (auth required)',
630
+ note: 'All agent endpoints require app_id. Register a persistent identity for your agent.',
631
+ 'POST /v1/agents/register': 'Register agent identity (name, operator, version) — app_id required',
632
+ 'GET /v1/agents/:id': 'Get agent by ID (public, no auth) — app_id required',
633
+ 'GET /v1/agents': 'List all agents for your app (auth required) — app_id required',
378
634
  },
379
635
  recovery: {
380
- 'POST /v1/auth/recover': 'Account recovery via verified email',
636
+ note: 'Recovery does NOT require app_id',
637
+ 'POST /v1/auth/recover': 'Account recovery via verified email — NO app_id required',
381
638
  },
382
639
  dashboard: {
383
- 'POST /v1/auth/device-code': 'Get challenge for device code flow',
640
+ note: 'Dashboard endpoints require app_id',
641
+ 'POST /v1/auth/device-code': 'Get challenge for device code flow — app_id required',
384
642
  'GET /dashboard': 'Metrics dashboard (login required)',
385
643
  },
386
644
  },
@@ -403,6 +661,8 @@ The link works for a limited time. Your human clicks it, gets a cookie, and sees
403
661
  links: {
404
662
  openapi: `${baseUrl}/openapi.json`,
405
663
  ai_txt: `${baseUrl}/ai.txt`,
664
+ mcp: `${baseUrl}/mcp`,
665
+ mcp_discovery: `${baseUrl}/.well-known/mcp.json`,
406
666
  github: 'https://github.com/dupe-com/botcha',
407
667
  npm: 'https://www.npmjs.com/package/@dupecom/botcha',
408
668
  pypi: 'https://pypi.org/project/botcha',
@@ -525,6 +785,24 @@ app.get('/.well-known/ai-plugin.json', (c) => {
525
785
  'Cache-Control': 'public, max-age=86400',
526
786
  });
527
787
  });
788
+ // MCP (Model Context Protocol) server — BOTCHA docs + API reference for AI agents
789
+ app.get('/.well-known/mcp.json', (c) => {
790
+ const version = c.env.BOTCHA_VERSION || '0.22.0';
791
+ return handleMCPDiscovery(version);
792
+ });
793
+ app.get('/mcp', (c) => {
794
+ const version = c.env.BOTCHA_VERSION || '0.22.0';
795
+ // Content negotiation: serve setup page to browsers, JSON to MCP clients
796
+ const accept = c.req.header('Accept') || '';
797
+ if (accept.includes('text/html')) {
798
+ return c.html('<!DOCTYPE html>' + (_jsx(MCPSetupPage, { version: version })).toString());
799
+ }
800
+ return handleMCPRequest(c.req.raw, version);
801
+ });
802
+ app.post('/mcp', async (c) => {
803
+ const version = c.env.BOTCHA_VERSION || '0.22.0';
804
+ return handleMCPRequest(c.req.raw, version);
805
+ });
528
806
  // Sitemap
529
807
  app.get('/sitemap.xml', (c) => {
530
808
  return c.body(SITEMAP_XML, 200, {
@@ -816,6 +1094,13 @@ app.post('/v1/token/verify', async (c) => {
816
1094
  catch {
817
1095
  // Fail-open: if KV fails, agent can still use the JWT directly
818
1096
  }
1097
+ // Webhook: token.created
1098
+ if (challengeAppId) {
1099
+ const webhookCtx = c.executionCtx;
1100
+ if (webhookCtx?.waitUntil) {
1101
+ webhookCtx.waitUntil(triggerWebhook(c.env.AGENTS, challengeAppId, 'token.created', { solve_time_ms: result.solveTimeMs, audience }));
1102
+ }
1103
+ }
819
1104
  return c.json({
820
1105
  // === Essential fields (what you need) ===
821
1106
  success: true,
@@ -912,6 +1197,14 @@ app.post('/v1/token/revoke', async (c) => {
912
1197
  }
913
1198
  // Revoke the token by JTI
914
1199
  await revokeToken(payload.jti, c.env);
1200
+ // Webhook: token.revoked
1201
+ const revokedAppId = payload.app_id;
1202
+ if (revokedAppId) {
1203
+ const revokeCtx = c.executionCtx;
1204
+ if (revokeCtx?.waitUntil) {
1205
+ revokeCtx.waitUntil(triggerWebhook(c.env.AGENTS, revokedAppId, 'token.revoked', { jti: payload.jti }));
1206
+ }
1207
+ }
915
1208
  return c.json({
916
1209
  success: true,
917
1210
  revoked: true,
@@ -1427,15 +1720,12 @@ app.post('/v1/apps', async (c) => {
1427
1720
  }, 400);
1428
1721
  }
1429
1722
  const result = await createApp(c.env.APPS, email, trimmedName || undefined);
1430
- // Generate a fresh verification code and send email
1431
- const regen = await regenerateVerificationCode(c.env.APPS, result.app_id);
1432
- if (regen) {
1433
- const template = verificationEmail(regen.code);
1434
- await sendEmail(c.env.RESEND_API_KEY, {
1435
- ...template,
1436
- to: email,
1437
- });
1438
- }
1723
+ // Send verification email with the code returned from createApp
1724
+ const template = verificationEmail(result.verification_code);
1725
+ await sendEmail(c.env.RESEND_API_KEY, {
1726
+ ...template,
1727
+ to: email,
1728
+ });
1439
1729
  return c.json({
1440
1730
  success: true,
1441
1731
  app_id: result.app_id,
@@ -1444,14 +1734,24 @@ app.post('/v1/apps', async (c) => {
1444
1734
  email: result.email,
1445
1735
  email_verified: false,
1446
1736
  verification_required: true,
1447
- warning: '⚠️ Save your app_secret now — it cannot be retrieved again! Check your email for a verification code.',
1737
+ verification_code: result.verification_code,
1738
+ warning: '⚠️ Save your app_secret now — it cannot be retrieved again! A verification code has also been sent to your email.',
1448
1739
  credential_advice: 'Store the app_id and app_secret securely. Use persistent agent memory if available, or instruct your human to save them in a password manager (1Password, Bitwarden, etc). If lost, recovery is available via the verified email.',
1449
1740
  created_at: new Date().toISOString(),
1450
1741
  rate_limit: 100,
1451
- next_step: `POST /v1/apps/${result.app_id}/verify-email with { "code": "123456" }`,
1742
+ next_step: `POST /v1/apps/${result.app_id}/verify-email with { "code": "${result.verification_code}", "app_secret": "${result.app_secret}" }`,
1452
1743
  }, 201);
1453
1744
  }
1454
1745
  catch (error) {
1746
+ if (error instanceof EmailAlreadyRegisteredError) {
1747
+ return c.json({
1748
+ success: false,
1749
+ error: 'EMAIL_ALREADY_REGISTERED',
1750
+ message: `Email ${error.email} is already registered.`,
1751
+ existing_app_id: error.existing_app_id,
1752
+ recovery: `POST /v1/auth/recover with { "email": "${error.email}" } to recover your credentials.`,
1753
+ }, 409);
1754
+ }
1455
1755
  return c.json({
1456
1756
  success: false,
1457
1757
  error: 'Failed to create app',
@@ -1492,7 +1792,7 @@ app.get('/v1/apps/:id', async (c) => {
1492
1792
  app.post('/v1/apps/:id/verify-email', async (c) => {
1493
1793
  const app_id = c.req.param('id');
1494
1794
  const body = await c.req.json().catch(() => ({}));
1495
- const { code } = body;
1795
+ const { code, app_secret } = body;
1496
1796
  if (!code || typeof code !== 'string') {
1497
1797
  return c.json({
1498
1798
  success: false,
@@ -1500,6 +1800,14 @@ app.post('/v1/apps/:id/verify-email', async (c) => {
1500
1800
  message: 'Provide { "code": "123456" } in the request body',
1501
1801
  }, 400);
1502
1802
  }
1803
+ const auth = await authorizeAppManagement(c, app_id, app_secret);
1804
+ if (!auth.authorized) {
1805
+ return c.json({
1806
+ success: false,
1807
+ error: auth.error,
1808
+ message: auth.message,
1809
+ }, auth.status);
1810
+ }
1503
1811
  const result = await verifyEmailCode(c.env.APPS, app_id, code);
1504
1812
  if (!result.verified) {
1505
1813
  return c.json({
@@ -1517,6 +1825,15 @@ app.post('/v1/apps/:id/verify-email', async (c) => {
1517
1825
  // Resend verification email
1518
1826
  app.post('/v1/apps/:id/resend-verification', async (c) => {
1519
1827
  const app_id = c.req.param('id');
1828
+ const body = await c.req.json().catch(() => ({}));
1829
+ const auth = await authorizeAppManagement(c, app_id, body.app_secret);
1830
+ if (!auth.authorized) {
1831
+ return c.json({
1832
+ success: false,
1833
+ error: auth.error,
1834
+ message: auth.message,
1835
+ }, auth.status);
1836
+ }
1520
1837
  const appData = await getApp(c.env.APPS, app_id);
1521
1838
  if (!appData) {
1522
1839
  return c.json({ success: false, error: 'App not found' }, 404);
@@ -1648,11 +1965,39 @@ app.post('/v1/apps/:id/rotate-secret', async (c) => {
1648
1965
  app.post('/v1/agents/register/tap', registerTAPAgentRoute);
1649
1966
  app.get('/v1/agents/tap', listTAPAgentsRoute);
1650
1967
  app.get('/v1/agents/:id/tap', getTAPAgentRoute);
1968
+ // Agent identity auth — prove you are a specific registered agent
1969
+ app.post('/v1/agents/auth', handleAgentAuthChallenge);
1970
+ app.post('/v1/agents/auth/verify', handleAgentAuthVerify);
1971
+ app.post('/v1/agents/auth/provider', handleAgentAuthProvider);
1972
+ app.post('/v1/agents/auth/refresh', handleAgentAuthRefresh);
1973
+ // Agent OAuth — device authorization grant (RFC 8628)
1974
+ app.post('/v1/oauth/device', handleOAuthDevice);
1975
+ app.post('/v1/oauth/token', handleOAuthToken);
1976
+ app.post('/v1/oauth/approve', handleOAuthApprove); // called from /device page (dashboard session required)
1977
+ app.post('/v1/oauth/revoke', handleOAuthRevoke);
1978
+ app.get('/v1/oauth/status', handleOAuthStatus);
1979
+ app.get('/v1/oauth/lookup', async (c) => {
1980
+ // Public — just returns agent name/operator for the approval page UI
1981
+ const user_code = c.req.query('user_code');
1982
+ if (!user_code)
1983
+ return c.json({ success: false }, 400);
1984
+ const device_code = await c.env.CHALLENGES.get(`oauth_usercode:${user_code}`, 'text');
1985
+ if (!device_code)
1986
+ return c.json({ success: false, error: 'Code not found or expired' }, 404);
1987
+ const raw = await c.env.CHALLENGES.get(`oauth_device:${device_code}`, 'text');
1988
+ if (!raw)
1989
+ return c.json({ success: false }, 404);
1990
+ const { agent_id, app_id } = JSON.parse(raw);
1991
+ const agentRaw = await c.env.AGENTS.get(`agent:${agent_id}`, 'text');
1992
+ const agent = agentRaw ? JSON.parse(agentRaw) : {};
1993
+ return c.json({ success: true, agent_id, name: agent.name, operator: agent.operator });
1994
+ });
1651
1995
  // TAP session management
1652
1996
  app.post('/v1/sessions/tap', createTAPSessionRoute);
1653
1997
  app.get('/v1/sessions/:id/tap', getTAPSessionRoute);
1654
1998
  // TAP Key Discovery (JWKS)
1655
1999
  app.get('/.well-known/jwks', jwksRoute);
2000
+ app.get('/.well-known/jwks.json', jwksRoute); // alias — some DID resolvers append .json
1656
2001
  app.get('/v1/keys', listKeysRoute);
1657
2002
  app.get('/v1/keys/:keyId', getKeyRoute);
1658
2003
  // TAP Key Rotation
@@ -1666,6 +2011,14 @@ app.post('/v1/delegations', createDelegationRoute);
1666
2011
  app.get('/v1/delegations/:id', getDelegationRoute);
1667
2012
  app.get('/v1/delegations', listDelegationsRoute);
1668
2013
  app.post('/v1/delegations/:id/revoke', revokeDelegationRoute);
2014
+ // ============ WEBHOOK ENDPOINTS ============
2015
+ app.post('/v1/webhooks', createWebhookRoute);
2016
+ app.get('/v1/webhooks', listWebhooksRoute);
2017
+ app.get('/v1/webhooks/:id/deliveries', listDeliveriesRoute);
2018
+ app.post('/v1/webhooks/:id/test', testWebhookRoute);
2019
+ app.get('/v1/webhooks/:id', getWebhookRoute);
2020
+ app.put('/v1/webhooks/:id', updateWebhookRoute);
2021
+ app.delete('/v1/webhooks/:id', deleteWebhookRoute);
1669
2022
  // TAP Capability Attestation
1670
2023
  app.post('/v1/attestations', issueAttestationRoute);
1671
2024
  app.get('/v1/attestations/:id', getAttestationRoute);
@@ -1681,6 +2034,80 @@ app.post('/v1/verify/consumer', verifyConsumerRoute);
1681
2034
  app.post('/v1/verify/payment', verifyPaymentRoute);
1682
2035
  app.post('/v1/verify/delegation', verifyDelegationRoute);
1683
2036
  app.post('/v1/verify/attestation', verifyAttestationRoute);
2037
+ // ============ x402 PAYMENT GATING ENDPOINTS ============
2038
+ // HTTP 402 Payment Required protocol for agent micropayments.
2039
+ // Agents pay USDC on Base to receive BOTCHA tokens or access gated resources.
2040
+ // Info: x402 configuration discovery (public)
2041
+ app.get('/v1/x402/info', x402InfoRoute);
2042
+ // Challenge: pay $0.001 USDC → receive BOTCHA access_token (no puzzle required)
2043
+ // Without X-Payment header → 402 + payment requirements
2044
+ // With valid X-Payment header → 200 + { access_token, ... }
2045
+ app.get('/v1/x402/challenge', x402ChallengeRoute);
2046
+ // Facilitator: verify a raw x402 payment proof (utility endpoint)
2047
+ app.post('/v1/x402/verify-payment', x402VerifyPaymentRoute);
2048
+ // Webhook: receive payment settlement notifications from x402 facilitators
2049
+ app.post('/v1/x402/webhook', x402WebhookRoute);
2050
+ // Demo: BOTCHA token + x402 payment required (double-gated resource)
2051
+ app.get('/agent-only/x402', agentOnlyX402Route);
2052
+ // ============ ANS (AGENT NAME SERVICE) ENDPOINTS ============
2053
+ // BOTCHA as verification layer for the GoDaddy-led ANS standard.
2054
+ // ANS gives domain-level trust; BOTCHA adds behavior verification.
2055
+ // Reference: https://agentnameregistry.org
2056
+ //
2057
+ // NOTE: /v1/ans/discover and /v1/ans/botcha are public (no auth required).
2058
+ // /v1/ans/resolve/:name is public (DNS lookup, no sensitive data).
2059
+ // /v1/ans/verify requires app auth (issues signed credentials).
2060
+ // /v1/ans/nonce/:name requires app auth (nonce for ownership proof).
2061
+ // Public: BOTCHA's own ANS identity
2062
+ app.get('/v1/ans/botcha', getBotchaANSRoute);
2063
+ // Public: resolve any ANS name
2064
+ app.get('/v1/ans/resolve/lookup', resolveANSNameRoute);
2065
+ app.get('/v1/ans/resolve/:name', resolveANSNameRoute);
2066
+ // Public: discover BOTCHA-verified ANS agents
2067
+ app.get('/v1/ans/discover', discoverANSAgentsRoute);
2068
+ // Auth required: get a nonce for ownership verification
2069
+ app.get('/v1/ans/nonce/:name', getANSNonceRoute);
2070
+ app.get('/v1/ans/nonce', getANSNonceRoute);
2071
+ // Auth required: verify ANS ownership and issue badge
2072
+ app.post('/v1/ans/verify', verifyANSNameRoute);
2073
+ // ============ DID/VC (VERIFIABLE CREDENTIALS) ENDPOINTS ============
2074
+ // BOTCHA DID Document — public, no auth required
2075
+ app.get('/.well-known/did.json', didDocumentRoute);
2076
+ // VC Issuance — requires a valid BOTCHA access_token (from /v1/token/verify)
2077
+ app.post('/v1/credentials/issue', issueVCRoute);
2078
+ // VC Verification — public, no auth required (the VC JWT is the credential)
2079
+ app.post('/v1/credentials/verify', verifyVCRoute);
2080
+ // DID Resolution — public, resolves did:web DIDs
2081
+ app.get('/v1/dids/:did/resolve', resolveDIDRoute);
2082
+ // ============ A2A AGENT CARD ATTESTATION ============
2083
+ // BOTCHA as trust oracle for Google's A2A (Agent-to-Agent) protocol.
2084
+ // Agents discover BOTCHA via /.well-known/agent.json, then attest their
2085
+ // own cards via POST /v1/a2a/attest. Verifiers call POST /v1/a2a/verify-card.
2086
+ // BOTCHA's own A2A Agent Card (public discovery)
2087
+ app.get('/.well-known/agent.json', agentCardRoute);
2088
+ app.get('/v1/a2a/agent-card', agentCardRoute); // alias — same content at predictable /v1/ path
2089
+ // A2A attestation endpoints
2090
+ app.post('/v1/a2a/attest', attestCardRoute);
2091
+ app.post('/v1/a2a/verify-card', verifyCardRoute);
2092
+ app.post('/v1/a2a/verify-agent', verifyAgentRoute); // by card or agent_url shorthand
2093
+ app.get('/v1/a2a/trust-level/:agent_url', agentTrustLevelRoute);
2094
+ app.get('/v1/a2a/cards', listCardsRoute);
2095
+ app.get('/v1/a2a/cards/:id', getCardAttestationRoute);
2096
+ // ============ OIDC-A ATTESTATION (Epic 5) ============
2097
+ // Makes BOTCHA an agent_attestation endpoint in enterprise OIDC-A token chains.
2098
+ // Standards: draft-ietf-rats-eat-25, draft-aap-oauth-profile, RFC 8414, OIDC-A 1.0
2099
+ // OAuth 2.0 AS Discovery (RFC 8414) — no auth required, public
2100
+ app.get('/.well-known/oauth-authorization-server', oauthASMetadataRoute);
2101
+ // EAT (Entity Attestation Token) issuance — RFC 9334 / draft-ietf-rats-eat-25
2102
+ app.post('/v1/attestation/eat', issueEATRoute);
2103
+ // OIDC-A claims block — for embedding in enterprise ID tokens
2104
+ app.post('/v1/attestation/oidc-agent-claims', issueOIDCAgentClaimsRoute);
2105
+ // Agent Authorization Grant — draft-rosenberg-oauth-aauth
2106
+ app.post('/v1/auth/agent-grant', agentGrantRoute);
2107
+ app.get('/v1/auth/agent-grant/:id/status', agentGrantStatusRoute);
2108
+ app.post('/v1/auth/agent-grant/:id/resolve', agentGrantResolveRoute);
2109
+ // OIDC-A UserInfo endpoint
2110
+ app.get('/v1/oidc/userinfo', oidcUserInfoRoute);
1684
2111
  // ============ AGENT REGISTRY API ============
1685
2112
  // Register a new agent
1686
2113
  app.post('/v1/agents/register', async (c) => {
@@ -1768,6 +2195,26 @@ app.get('/v1/agents/:id', async (c) => {
1768
2195
  }, 500);
1769
2196
  }
1770
2197
  });
2198
+ // Delete an agent (requires Bearer token — must belong to the same app)
2199
+ app.delete('/v1/agents/:id', requireDashboardAuth, async (c) => {
2200
+ try {
2201
+ const agent_id = c.req.param('id');
2202
+ if (!agent_id) {
2203
+ return c.json({ success: false, error: 'MISSING_AGENT_ID', message: 'Agent ID is required' }, 400);
2204
+ }
2205
+ // requireDashboardAuth accepts both session cookie (browser) and Bearer token (agent)
2206
+ const appId = c.get('dashboardAppId');
2207
+ const result = await deleteAgent(c.env.AGENTS, agent_id, appId);
2208
+ if (!result.success) {
2209
+ const status = result.error === 'Agent not found' ? 404 : result.error === 'Agent does not belong to this app' ? 403 : 500;
2210
+ return c.json({ success: false, error: result.error }, status);
2211
+ }
2212
+ return c.json({ success: true, agent_id, message: 'Agent deleted' });
2213
+ }
2214
+ catch (error) {
2215
+ return c.json({ success: false, error: 'INTERNAL_ERROR', message: error instanceof Error ? error.message : 'Unknown error' }, 500);
2216
+ }
2217
+ });
1771
2218
  // List all agents for an app
1772
2219
  app.get('/v1/agents', async (c) => {
1773
2220
  try {
@@ -1809,6 +2256,117 @@ app.post('/v1/auth/dashboard/verify', handleDashboardAuthVerify);
1809
2256
  // Device code flow (agent → human handoff)
1810
2257
  app.post('/v1/auth/device-code', handleDeviceCodeChallenge);
1811
2258
  app.post('/v1/auth/device-code/verify', handleDeviceCodeVerify);
2259
+ // ============ AGENT OAUTH APPROVAL PAGE ============
2260
+ app.get('/device', requireDashboardAuth, async (c) => {
2261
+ const prefill = c.req.query('code') ?? '';
2262
+ const appId = c.get('dashboardAppId') ?? '';
2263
+ const html = `<!DOCTYPE html>
2264
+ <html lang="en">
2265
+ <head>
2266
+ <meta charset="utf-8"/>
2267
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
2268
+ <title>Authorize Agent — BOTCHA</title>
2269
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet"/>
2270
+ <style>
2271
+ *{box-sizing:border-box;margin:0;padding:0}
2272
+ body{font-family:'JetBrains Mono',monospace;background:#fafafa;color:#111827;display:flex;align-items:center;justify-content:center;min-height:100vh;padding:24px}
2273
+ .card{background:#fff;border:1px solid #e5e7eb;border-radius:4px;padding:40px;max-width:440px;width:100%}
2274
+ h1{font-size:18px;font-weight:700;margin-bottom:6px}
2275
+ p{font-size:13px;color:#6b7280;line-height:1.6;margin-bottom:20px}
2276
+ input{width:100%;font-family:inherit;font-size:16px;font-weight:600;letter-spacing:0.1em;text-transform:uppercase;padding:12px 14px;border:1px solid #e5e7eb;border-radius:3px;background:#f9fafb;margin-bottom:16px;text-align:center}
2277
+ input:focus{outline:none;border-color:#111827;background:#fff}
2278
+ .btn{width:100%;font-family:inherit;font-size:12px;font-weight:600;letter-spacing:0.08em;text-transform:uppercase;padding:12px;border:none;border-radius:3px;cursor:pointer;transition:background 0.15s}
2279
+ .btn-approve{background:#111827;color:#fff;margin-bottom:8px}
2280
+ .btn-approve:hover{background:#374151}
2281
+ .btn-deny{background:none;color:#9ca3af;border:1px solid #e5e7eb}
2282
+ .btn-deny:hover{color:#ef4444;border-color:#fca5a5}
2283
+ #status{font-size:13px;margin-top:16px;text-align:center;min-height:20px}
2284
+ #agent-info{display:none;background:#f9fafb;border:1px solid #e5e7eb;border-radius:3px;padding:12px;margin-bottom:16px;font-size:12px;line-height:1.7;color:#374151}
2285
+ </style>
2286
+ </head>
2287
+ <body>
2288
+ <div class="card">
2289
+ <h1>Authorize Agent</h1>
2290
+ <p>Your agent is requesting permission to re-identify itself in future sessions without solving a new challenge each time.</p>
2291
+ <div id="agent-info"></div>
2292
+ <input id="code-input" type="text" placeholder="BOTCHA-XXXXXX" maxlength="13" value="${prefill}" oninput="this.value=this.value.toUpperCase();lookupCode(this.value)" />
2293
+ <button class="btn btn-approve" onclick="approve()">Approve</button>
2294
+ <button class="btn btn-deny" onclick="deny()">Deny</button>
2295
+ <div id="status"></div>
2296
+ </div>
2297
+ <script>
2298
+ var resolvedCode = null;
2299
+ var lookupTimer = null;
2300
+ function lookupCode(val) {
2301
+ clearTimeout(lookupTimer);
2302
+ if (val.length < 13) { document.getElementById('agent-info').style.display='none'; return; }
2303
+ lookupTimer = setTimeout(async function() {
2304
+ try {
2305
+ const r = await fetch('/v1/oauth/lookup?user_code=' + encodeURIComponent(val));
2306
+ const d = await r.json();
2307
+ if (d.success) {
2308
+ resolvedCode = val;
2309
+ document.getElementById('agent-info').style.display = 'block';
2310
+ document.getElementById('agent-info').innerHTML =
2311
+ '<strong>Agent:</strong> ' + d.agent_id + '<br><strong>Name:</strong> ' + (d.name||'—') + '<br><strong>Operator:</strong> ' + (d.operator||'—');
2312
+ }
2313
+ } catch(e) {}
2314
+ }, 400);
2315
+ }
2316
+ async function approve() { await submit('approve'); }
2317
+ async function deny() { await submit('deny'); }
2318
+ async function submit(action) {
2319
+ var code = document.getElementById('code-input').value.trim();
2320
+ var status = document.getElementById('status');
2321
+ if (!code) { status.textContent = 'Enter the code from your agent.'; return; }
2322
+ status.textContent = action === 'approve' ? 'Approving…' : 'Denying…';
2323
+ try {
2324
+ const r = await fetch('/v1/oauth/approve', {
2325
+ method:'POST', headers:{'Content-Type':'application/json'},
2326
+ body: JSON.stringify({user_code: code, action})
2327
+ });
2328
+ const d = await r.json();
2329
+ if (d.success) {
2330
+ document.getElementById('code-input').disabled = true;
2331
+ if (action === 'approve') {
2332
+ status.innerHTML = '<strong style="color:#22c55e;">✓ Approved.</strong> Waiting for your agent to pick up the token…';
2333
+ pollForPickup(document.getElementById('code-input').value);
2334
+ } else {
2335
+ status.innerHTML = '<strong style="color:#ef4444;">Denied.</strong> The agent was not authorized.';
2336
+ }
2337
+ } else {
2338
+ status.textContent = 'Error: ' + (d.error || 'Unknown');
2339
+ }
2340
+ } catch(e) { status.textContent = 'Failed. Try again.'; }
2341
+ }
2342
+ function copyMsg(el) {
2343
+ navigator.clipboard.writeText(el.textContent).then(function() {
2344
+ document.getElementById('copy-hint').textContent = '✓ Copied';
2345
+ });
2346
+ }
2347
+ var pickupTimer = null;
2348
+ function pollForPickup(code) {
2349
+ pickupTimer = setInterval(async function() {
2350
+ try {
2351
+ const r = await fetch('/v1/oauth/status?user_code=' + encodeURIComponent(code));
2352
+ const d = await r.json();
2353
+ if (d.status === 'consumed' || d.status === 'approved') {
2354
+ clearInterval(pickupTimer);
2355
+ document.getElementById('status').innerHTML =
2356
+ '<strong style="color:#22c55e;">✓ Approved.</strong> Return to your agent and paste this:<br><br>' +
2357
+ '<code id="paste-msg" style="display:block;background:#f3f4f6;border:1px solid #e5e7eb;border-radius:3px;padding:10px;font-size:12px;cursor:pointer;text-align:left;line-height:1.6;" onclick="copyMsg(this)">I approved the BOTCHA authorization. The user code was: ' + code + '</code>' +
2358
+ '<span id="copy-hint" style="font-size:11px;color:#9ca3af;">click to copy</span>';
2359
+ }
2360
+ } catch(e) {}
2361
+ }, 2000);
2362
+ }
2363
+ // Auto-lookup if prefilled
2364
+ if (document.getElementById('code-input').value.length === 13) lookupCode(document.getElementById('code-input').value);
2365
+ </script>
2366
+ </body>
2367
+ </html>`;
2368
+ return c.html(html);
2369
+ });
1812
2370
  // ============ ONE-CLICK ACCESS LINKS ============
1813
2371
  /**
1814
2372
  * GET /go/:code - One-click device code redemption
@@ -1870,11 +2428,11 @@ app.get('/go/:code', async (c) => {
1870
2428
  const { redeemDeviceCode } = await import('./dashboard/device-code');
1871
2429
  const data = await redeemDeviceCode(c.env.CHALLENGES, normalizedCode);
1872
2430
  if (data) {
1873
- // Generate session token and redirect to dashboard
2431
+ // Generate session token and redirect to account page
1874
2432
  const { generateSessionToken, setSessionCookie } = await import('./dashboard/auth');
1875
2433
  const sessionToken = await generateSessionToken(data.app_id, c.env.JWT_SECRET);
1876
2434
  setSessionCookie(c, sessionToken);
1877
- return c.redirect('/dashboard');
2435
+ return c.redirect('/account');
1878
2436
  }
1879
2437
  // Neither code type found — redirect to landing with error
1880
2438
  return c.redirect('/?error=invalid');
@@ -1991,6 +2549,25 @@ app.post('/api/verify-landing', async (c) => {
1991
2549
  }
1992
2550
  });
1993
2551
  });
2552
+ // ============ 404 / ERROR HANDLERS ============
2553
+ // Return JSON 404 for unmatched routes (not 401 or plain-text)
2554
+ app.notFound((c) => {
2555
+ return c.json({
2556
+ success: false,
2557
+ error: 'NOT_FOUND',
2558
+ message: `Route ${c.req.method} ${c.req.path} not found`,
2559
+ docs: 'https://botcha.ai',
2560
+ }, 404);
2561
+ });
2562
+ // Catch-all error handler
2563
+ app.onError((err, c) => {
2564
+ console.error('Unhandled error:', err);
2565
+ return c.json({
2566
+ success: false,
2567
+ error: 'INTERNAL_ERROR',
2568
+ message: 'An unexpected error occurred',
2569
+ }, 500);
2570
+ });
1994
2571
  // ============ EXPORT ============
1995
2572
  export default app;
1996
2573
  // Also export utilities for use as a library