@dupecom/botcha-cloudflare 0.21.0 → 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 +9 -0
  12. package/dist/apps.d.ts.map +1 -1
  13. package/dist/apps.js +26 -0
  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 +3 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +452 -52
  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 +732 -1
  42. package/dist/static.d.ts.map +1 -1
  43. package/dist/static.js +646 -2
  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,28 +15,54 @@ 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, validateAppSecret } 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();
@@ -46,20 +72,10 @@ app.use('*', async (c, next) => {
46
72
  c.header('X-Botcha-Docs', 'https://botcha.ai/openapi.json');
47
73
  c.header('X-Botcha-Runtime', 'cloudflare-workers');
48
74
  });
49
- // App gate: require registered app_id with verified email on /v1/* routes.
50
- // Open paths (registration, verification, recovery) are exempted.
51
- const APP_GATE_OPEN_PATHS = [
52
- '/v1/apps', // POST: create app (registration)
53
- '/v1/auth/recover', // POST: account recovery
54
- ];
55
- // Pattern-match paths that start with /v1/apps/:id/ (verify-email, resend-verification, etc.)
56
- function isAppManagementPath(path) {
57
- return /^\/v1\/apps\/[^/]+\/(verify-email|resend-verification)$/.test(path);
58
- }
59
75
  app.use('/v1/*', async (c, next) => {
60
76
  const path = new URL(c.req.url).pathname;
61
77
  // Allow open paths through without app_id
62
- if (APP_GATE_OPEN_PATHS.includes(path) || isAppManagementPath(path)) {
78
+ if (shouldBypassAppGate(path, c.req.method)) {
63
79
  return next();
64
80
  }
65
81
  // Also allow GET /v1/apps/:id (app info lookup)
@@ -389,33 +405,74 @@ app.get('/', async (c) => {
389
405
  missing: 'No code was provided. Paste the prompt below into your AI agent to get a link.',
390
406
  };
391
407
  const error = errorParam ? errorMessages[errorParam] || undefined : undefined;
392
- 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 }));
393
410
  }
394
- // === UNVERIFIED: minimal teaser just enough to get started ===
411
+ // === UNVERIFIED: challenge-first entry point ===
395
412
  if (!isVerified) {
396
413
  const teaser = {
397
414
  name: 'BOTCHA',
398
415
  version,
399
416
  tagline: 'Prove you are a bot. Humans need not apply.',
400
417
  status: 'unverified',
401
- message: 'App registration required. Register to see the full API documentation.',
402
- registration_required: {
403
- note: 'All API endpoints require a registered app with verified email (v0.21.0)',
404
- demo: 'The homepage demo at botcha.ai lets agents try challenges without registering',
405
- step1: 'Ask your human for their email',
406
- step2: `POST ${baseUrl}/v1/apps {"email":"you@example.com","name":"My App"} NO app_id needed`,
407
- step3: 'Human receives 6-digit code via email',
408
- step4: `POST ${baseUrl}/v1/apps/{app_id}/verify-email {"code":"123456","app_secret":"sk_..."} NO app_id needed`,
409
- step5: 'Use your app_id on ALL other /v1/* endpoints via ?app_id=, X-App-Id header, or JWT claim',
410
- },
411
- human_handoff: {
412
- note: 'After verifying, the response includes a human_link. Give it to your human to open in their browser.',
413
- instruction: 'Tell your human: "Click this link to get access" and give them the human_link URL.',
414
- link_field: 'human_link from token verify response',
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
+ ],
415
429
  },
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
+ ],
416
472
  links: {
417
473
  openapi: `${baseUrl}/openapi.json`,
418
474
  ai_txt: `${baseUrl}/ai.txt`,
475
+ mcp: `${baseUrl}/mcp`,
419
476
  },
420
477
  };
421
478
  if (preference === 'markdown') {
@@ -423,34 +480,72 @@ app.get('/', async (c) => {
423
480
 
424
481
  > Prove you are a bot. Humans need not apply.
425
482
 
426
- **Status:** Unverified. Register an app to see the full documentation.
483
+ You have found BOTCHA the identity layer for AI agents.
484
+
485
+ ## Step 1: Prove you are an AI (do this now — use the demo app, no registration needed)
427
486
 
428
- ## App Registration Required (v0.21.0)
487
+ \`\`\`bash
488
+ GET ${baseUrl}/v1/challenges?app_id=${c.env.BOTCHA_INTERNAL_APP_ID}
429
489
 
430
- **All API endpoints require a registered app with verified email.**
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
+ \`\`\`
498
+
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.
431
503
 
432
- - Register for free: \`POST /v1/apps\` with your email
433
- - The homepage demo at botcha.ai lets agents try challenges without registering
434
- - Provide app_id via \`?app_id=\`, \`X-App-Id\` header, or JWT claim
504
+ On success you receive a verified badge you are a confirmed AI agent.
505
+
506
+ ---
435
507
 
436
- ## Register Your App
508
+ ## Step 2: Ask your human what they want to do
437
509
 
438
- 1. Ask your human for their email
439
- 2. \`POST ${baseUrl}/v1/apps {"email":"you@example.com","name":"My App"}\` — **NO app_id needed**
440
- 3. Human receives 6-digit code via email
441
- 4. \`POST ${baseUrl}/v1/apps/{app_id}/verify-email {"code":"123456","app_secret":"sk_..."}\` — **NO app_id needed**
442
- 5. Use your \`app_id\` on ALL other \`/v1/*\` endpoints via \`?app_id=\`, \`X-App-Id\` header, or JWT claim
510
+ Present these two options to your human. Use a numbered list, buttons, or your native selection UI. Wait for their choice.
443
511
 
444
- ## Human Handoff
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.
445
520
 
446
- After verifying, the response includes a \`human_link\`. Give this URL to your human:
447
- **"Click this link to get access: {human_link}"**
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.
448
523
 
449
- The link works for a limited time. Your human clicks it, gets a cookie, and sees the verified site.
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
450
545
 
451
546
  ---
452
547
 
453
- *Full API docs, SDKs, onboarding, and endpoint reference available after registration.*
548
+ More: [OpenAPI](${baseUrl}/openapi.json) · [ai.txt](${baseUrl}/ai.txt) · [MCP](${baseUrl}/mcp)
454
549
  `;
455
550
  return c.body(md, 200, {
456
551
  'Content-Type': 'text/markdown; charset=utf-8',
@@ -517,7 +612,7 @@ The link works for a limited time. Your human clicks it, gets a cookie, and sees
517
612
  'POST /v1/token/verify': 'Submit solution → access_token (1hr) + refresh_token (1hr) — app_id required',
518
613
  'POST /v1/token/refresh': 'Refresh access token — app_id required',
519
614
  'POST /v1/token/revoke': 'Revoke a token — app_id required',
520
- 'POST /v1/token/validate': 'Remote token validation — verify any BOTCHA token without needing the secret — 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.',
521
616
  },
522
617
  protected: {
523
618
  'GET /agent-only': 'Demo protected endpoint — requires Bearer token (app_id required)',
@@ -566,6 +661,8 @@ The link works for a limited time. Your human clicks it, gets a cookie, and sees
566
661
  links: {
567
662
  openapi: `${baseUrl}/openapi.json`,
568
663
  ai_txt: `${baseUrl}/ai.txt`,
664
+ mcp: `${baseUrl}/mcp`,
665
+ mcp_discovery: `${baseUrl}/.well-known/mcp.json`,
569
666
  github: 'https://github.com/dupe-com/botcha',
570
667
  npm: 'https://www.npmjs.com/package/@dupecom/botcha',
571
668
  pypi: 'https://pypi.org/project/botcha',
@@ -688,6 +785,24 @@ app.get('/.well-known/ai-plugin.json', (c) => {
688
785
  'Cache-Control': 'public, max-age=86400',
689
786
  });
690
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
+ });
691
806
  // Sitemap
692
807
  app.get('/sitemap.xml', (c) => {
693
808
  return c.body(SITEMAP_XML, 200, {
@@ -979,6 +1094,13 @@ app.post('/v1/token/verify', async (c) => {
979
1094
  catch {
980
1095
  // Fail-open: if KV fails, agent can still use the JWT directly
981
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
+ }
982
1104
  return c.json({
983
1105
  // === Essential fields (what you need) ===
984
1106
  success: true,
@@ -1075,6 +1197,14 @@ app.post('/v1/token/revoke', async (c) => {
1075
1197
  }
1076
1198
  // Revoke the token by JTI
1077
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
+ }
1078
1208
  return c.json({
1079
1209
  success: true,
1080
1210
  revoked: true,
@@ -1604,14 +1734,24 @@ app.post('/v1/apps', async (c) => {
1604
1734
  email: result.email,
1605
1735
  email_verified: false,
1606
1736
  verification_required: true,
1607
- 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.',
1608
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.',
1609
1740
  created_at: new Date().toISOString(),
1610
1741
  rate_limit: 100,
1611
- next_step: `POST /v1/apps/${result.app_id}/verify-email with { "code": "123456", "app_secret": "${result.app_secret}" }`,
1742
+ next_step: `POST /v1/apps/${result.app_id}/verify-email with { "code": "${result.verification_code}", "app_secret": "${result.app_secret}" }`,
1612
1743
  }, 201);
1613
1744
  }
1614
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
+ }
1615
1755
  return c.json({
1616
1756
  success: false,
1617
1757
  error: 'Failed to create app',
@@ -1825,11 +1965,39 @@ app.post('/v1/apps/:id/rotate-secret', async (c) => {
1825
1965
  app.post('/v1/agents/register/tap', registerTAPAgentRoute);
1826
1966
  app.get('/v1/agents/tap', listTAPAgentsRoute);
1827
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
+ });
1828
1995
  // TAP session management
1829
1996
  app.post('/v1/sessions/tap', createTAPSessionRoute);
1830
1997
  app.get('/v1/sessions/:id/tap', getTAPSessionRoute);
1831
1998
  // TAP Key Discovery (JWKS)
1832
1999
  app.get('/.well-known/jwks', jwksRoute);
2000
+ app.get('/.well-known/jwks.json', jwksRoute); // alias — some DID resolvers append .json
1833
2001
  app.get('/v1/keys', listKeysRoute);
1834
2002
  app.get('/v1/keys/:keyId', getKeyRoute);
1835
2003
  // TAP Key Rotation
@@ -1843,6 +2011,14 @@ app.post('/v1/delegations', createDelegationRoute);
1843
2011
  app.get('/v1/delegations/:id', getDelegationRoute);
1844
2012
  app.get('/v1/delegations', listDelegationsRoute);
1845
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);
1846
2022
  // TAP Capability Attestation
1847
2023
  app.post('/v1/attestations', issueAttestationRoute);
1848
2024
  app.get('/v1/attestations/:id', getAttestationRoute);
@@ -1858,6 +2034,80 @@ app.post('/v1/verify/consumer', verifyConsumerRoute);
1858
2034
  app.post('/v1/verify/payment', verifyPaymentRoute);
1859
2035
  app.post('/v1/verify/delegation', verifyDelegationRoute);
1860
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);
1861
2111
  // ============ AGENT REGISTRY API ============
1862
2112
  // Register a new agent
1863
2113
  app.post('/v1/agents/register', async (c) => {
@@ -1945,6 +2195,26 @@ app.get('/v1/agents/:id', async (c) => {
1945
2195
  }, 500);
1946
2196
  }
1947
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
+ });
1948
2218
  // List all agents for an app
1949
2219
  app.get('/v1/agents', async (c) => {
1950
2220
  try {
@@ -1986,6 +2256,117 @@ app.post('/v1/auth/dashboard/verify', handleDashboardAuthVerify);
1986
2256
  // Device code flow (agent → human handoff)
1987
2257
  app.post('/v1/auth/device-code', handleDeviceCodeChallenge);
1988
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
+ });
1989
2370
  // ============ ONE-CLICK ACCESS LINKS ============
1990
2371
  /**
1991
2372
  * GET /go/:code - One-click device code redemption
@@ -2047,11 +2428,11 @@ app.get('/go/:code', async (c) => {
2047
2428
  const { redeemDeviceCode } = await import('./dashboard/device-code');
2048
2429
  const data = await redeemDeviceCode(c.env.CHALLENGES, normalizedCode);
2049
2430
  if (data) {
2050
- // Generate session token and redirect to dashboard
2431
+ // Generate session token and redirect to account page
2051
2432
  const { generateSessionToken, setSessionCookie } = await import('./dashboard/auth');
2052
2433
  const sessionToken = await generateSessionToken(data.app_id, c.env.JWT_SECRET);
2053
2434
  setSessionCookie(c, sessionToken);
2054
- return c.redirect('/dashboard');
2435
+ return c.redirect('/account');
2055
2436
  }
2056
2437
  // Neither code type found — redirect to landing with error
2057
2438
  return c.redirect('/?error=invalid');
@@ -2168,6 +2549,25 @@ app.post('/api/verify-landing', async (c) => {
2168
2549
  }
2169
2550
  });
2170
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
+ });
2171
2571
  // ============ EXPORT ============
2172
2572
  export default app;
2173
2573
  // Also export utilities for use as a library