@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.
- package/README.md +74 -9
- package/dist/agent-auth.d.ts +129 -0
- package/dist/agent-auth.d.ts.map +1 -0
- package/dist/agent-auth.js +210 -0
- package/dist/agents.d.ts +10 -0
- package/dist/agents.d.ts.map +1 -1
- package/dist/agents.js +51 -1
- package/dist/app-gate.d.ts +6 -0
- package/dist/app-gate.d.ts.map +1 -0
- package/dist/app-gate.js +69 -0
- package/dist/apps.d.ts +9 -0
- package/dist/apps.d.ts.map +1 -1
- package/dist/apps.js +26 -0
- package/dist/dashboard/account.d.ts +63 -0
- package/dist/dashboard/account.d.ts.map +1 -0
- package/dist/dashboard/account.js +488 -0
- package/dist/dashboard/api.js +15 -68
- package/dist/dashboard/auth.d.ts.map +1 -1
- package/dist/dashboard/auth.js +14 -14
- package/dist/dashboard/docs.d.ts.map +1 -1
- package/dist/dashboard/docs.js +146 -3
- package/dist/dashboard/layout.d.ts.map +1 -1
- package/dist/dashboard/layout.js +2 -2
- package/dist/dashboard/mcp-setup.d.ts +15 -0
- package/dist/dashboard/mcp-setup.d.ts.map +1 -0
- package/dist/dashboard/mcp-setup.js +391 -0
- package/dist/dashboard/showcase.d.ts +6 -10
- package/dist/dashboard/showcase.d.ts.map +1 -1
- package/dist/dashboard/showcase.js +67 -991
- package/dist/dashboard/whitepaper.d.ts.map +1 -1
- package/dist/dashboard/whitepaper.js +42 -4
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +452 -52
- package/dist/mcp.d.ts +20 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +1290 -0
- package/dist/oauth-agent.d.ts +130 -0
- package/dist/oauth-agent.d.ts.map +1 -0
- package/dist/oauth-agent.js +194 -0
- package/dist/static.d.ts +732 -1
- package/dist/static.d.ts.map +1 -1
- package/dist/static.js +646 -2
- package/dist/tap-a2a-routes.d.ts +355 -0
- package/dist/tap-a2a-routes.d.ts.map +1 -0
- package/dist/tap-a2a-routes.js +475 -0
- package/dist/tap-a2a.d.ts +199 -0
- package/dist/tap-a2a.d.ts.map +1 -0
- package/dist/tap-a2a.js +502 -0
- package/dist/tap-agents.d.ts +15 -0
- package/dist/tap-agents.d.ts.map +1 -1
- package/dist/tap-agents.js +31 -1
- package/dist/tap-ans-routes.d.ts +302 -0
- package/dist/tap-ans-routes.d.ts.map +1 -0
- package/dist/tap-ans-routes.js +535 -0
- package/dist/tap-ans.d.ts +241 -0
- package/dist/tap-ans.d.ts.map +1 -0
- package/dist/tap-ans.js +481 -0
- package/dist/tap-delegation-routes.d.ts.map +1 -1
- package/dist/tap-delegation-routes.js +11 -0
- package/dist/tap-did.d.ts +140 -0
- package/dist/tap-did.d.ts.map +1 -0
- package/dist/tap-did.js +262 -0
- package/dist/tap-oidca-routes.d.ts +383 -0
- package/dist/tap-oidca-routes.d.ts.map +1 -0
- package/dist/tap-oidca-routes.js +597 -0
- package/dist/tap-oidca.d.ts +288 -0
- package/dist/tap-oidca.d.ts.map +1 -0
- package/dist/tap-oidca.js +461 -0
- package/dist/tap-routes.d.ts +24 -8
- package/dist/tap-routes.d.ts.map +1 -1
- package/dist/tap-routes.js +169 -23
- package/dist/tap-vc-routes.d.ts +358 -0
- package/dist/tap-vc-routes.d.ts.map +1 -0
- package/dist/tap-vc-routes.js +367 -0
- package/dist/tap-vc.d.ts +125 -0
- package/dist/tap-vc.d.ts.map +1 -0
- package/dist/tap-vc.js +245 -0
- package/dist/tap-x402-routes.d.ts +89 -0
- package/dist/tap-x402-routes.d.ts.map +1 -0
- package/dist/tap-x402-routes.js +579 -0
- package/dist/tap-x402.d.ts +222 -0
- package/dist/tap-x402.d.ts.map +1 -0
- package/dist/tap-x402.js +546 -0
- package/dist/webhooks.d.ts +99 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +642 -0
- 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 {
|
|
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 (
|
|
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
|
-
|
|
408
|
+
const baseUrl = `${url.protocol}//${url.host}`;
|
|
409
|
+
return c.html(_jsx(ShowcasePage, { version: version, error: error, baseUrl: baseUrl }));
|
|
393
410
|
}
|
|
394
|
-
// === UNVERIFIED:
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
step1:
|
|
406
|
-
step2: `POST ${baseUrl}/v1/
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
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
|
-
|
|
487
|
+
\`\`\`bash
|
|
488
|
+
GET ${baseUrl}/v1/challenges?app_id=${c.env.BOTCHA_INTERNAL_APP_ID}
|
|
429
489
|
|
|
430
|
-
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
504
|
+
On success you receive a verified badge — you are a confirmed AI agent.
|
|
505
|
+
|
|
506
|
+
---
|
|
435
507
|
|
|
436
|
-
##
|
|
508
|
+
## Step 2: Ask your human what they want to do
|
|
437
509
|
|
|
438
|
-
|
|
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
|
-
|
|
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
|
-
|
|
447
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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": "
|
|
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
|
|
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('/
|
|
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
|