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