@dupecom/botcha-cloudflare 0.10.0 → 0.11.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 +9 -8
- package/dist/agents.d.ts +68 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +123 -0
- package/dist/dashboard/api.js +20 -13
- package/dist/dashboard/auth.d.ts.map +1 -1
- package/dist/dashboard/auth.js +6 -6
- package/dist/dashboard/landing.d.ts +20 -0
- package/dist/dashboard/landing.d.ts.map +1 -0
- package/dist/dashboard/landing.js +45 -0
- package/dist/dashboard/layout.d.ts +7 -0
- package/dist/dashboard/layout.d.ts.map +1 -1
- package/dist/dashboard/layout.js +17 -0
- package/dist/dashboard/styles.d.ts +1 -1
- package/dist/dashboard/styles.d.ts.map +1 -1
- package/dist/dashboard/styles.js +99 -1
- package/dist/email.d.ts +0 -3
- package/dist/email.d.ts.map +1 -1
- package/dist/email.js +1 -4
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +461 -212
- package/dist/routes/stream.js +1 -1
- package/dist/static.d.ts +179 -2
- package/dist/static.d.ts.map +1 -1
- package/dist/static.js +319 -5
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
1
2
|
/**
|
|
2
|
-
* BOTCHA - Cloudflare Workers Edition v0.
|
|
3
|
+
* BOTCHA - Cloudflare Workers Edition v0.11.0
|
|
3
4
|
*
|
|
4
5
|
* Prove you're a bot. Humans need not apply.
|
|
5
6
|
*
|
|
@@ -8,15 +9,18 @@
|
|
|
8
9
|
import { Hono } from 'hono';
|
|
9
10
|
import { cors } from 'hono/cors';
|
|
10
11
|
import { generateSpeedChallenge, verifySpeedChallenge, generateStandardChallenge, verifyStandardChallenge, generateReasoningChallenge, verifyReasoningChallenge, generateHybridChallenge, verifyHybridChallenge, verifyLandingChallenge, validateLandingToken, } from './challenges';
|
|
12
|
+
import { SignJWT, jwtVerify } from 'jose';
|
|
11
13
|
import { generateToken, verifyToken, extractBearerToken, revokeToken, refreshAccessToken } from './auth';
|
|
12
14
|
import { checkRateLimit, getClientIP } from './rate-limit';
|
|
13
15
|
import { verifyBadge, generateBadgeSvg, generateBadgeHtml, createBadgeResponse } from './badge';
|
|
14
16
|
import streamRoutes from './routes/stream';
|
|
15
17
|
import dashboardRoutes from './dashboard/index';
|
|
16
18
|
import { handleDashboardAuthChallenge, handleDashboardAuthVerify, handleDeviceCodeChallenge, handleDeviceCodeVerify, } from './dashboard/auth';
|
|
17
|
-
import { ROBOTS_TXT, AI_TXT, AI_PLUGIN_JSON, SITEMAP_XML, getOpenApiSpec } from './static';
|
|
19
|
+
import { ROBOTS_TXT, AI_TXT, AI_PLUGIN_JSON, SITEMAP_XML, getOpenApiSpec, getBotchaMarkdown } from './static';
|
|
18
20
|
import { createApp, getApp, getAppByEmail, verifyEmailCode, rotateAppSecret, regenerateVerificationCode } from './apps';
|
|
19
21
|
import { sendEmail, verificationEmail, recoveryEmail, secretRotatedEmail } from './email';
|
|
22
|
+
import { LandingPage, VerifiedLandingPage } from './dashboard/landing';
|
|
23
|
+
import { createAgent, getAgent, listAgents } from './agents';
|
|
20
24
|
import { trackChallengeGenerated, trackChallengeVerified, trackAuthAttempt, trackRateLimitExceeded, } from './analytics';
|
|
21
25
|
const app = new Hono();
|
|
22
26
|
// ============ MIDDLEWARE ============
|
|
@@ -27,7 +31,7 @@ app.route('/dashboard', dashboardRoutes);
|
|
|
27
31
|
// BOTCHA discovery headers
|
|
28
32
|
app.use('*', async (c, next) => {
|
|
29
33
|
await next();
|
|
30
|
-
c.header('X-Botcha-Version', c.env.BOTCHA_VERSION || '0.
|
|
34
|
+
c.header('X-Botcha-Version', c.env.BOTCHA_VERSION || '0.11.0');
|
|
31
35
|
c.header('X-Botcha-Enabled', 'true');
|
|
32
36
|
c.header('X-Botcha-Methods', 'speed-challenge,reasoning-challenge,hybrid-challenge,standard-challenge,jwt-token');
|
|
33
37
|
c.header('X-Botcha-Docs', 'https://botcha.ai/openapi.json');
|
|
@@ -95,215 +99,200 @@ async function requireJWT(c, next) {
|
|
|
95
99
|
await next();
|
|
96
100
|
}
|
|
97
101
|
// ============ ROOT & INFO ============
|
|
98
|
-
// Detect
|
|
99
|
-
|
|
102
|
+
// Detect request preference: 'markdown' | 'json' | 'html'
|
|
103
|
+
// Agents like Claude Code and OpenCode send Accept: text/markdown
|
|
104
|
+
function detectAcceptPreference(c) {
|
|
100
105
|
const accept = c.req.header('accept') || '';
|
|
101
|
-
const userAgent = c.req.header('user-agent') || '';
|
|
102
|
-
//
|
|
106
|
+
const userAgent = (c.req.header('user-agent') || '').toLowerCase();
|
|
107
|
+
// Explicit markdown preference (Cloudflare Markdown for Agents convention)
|
|
108
|
+
if (accept.includes('text/markdown'))
|
|
109
|
+
return 'markdown';
|
|
110
|
+
// Explicit JSON preference
|
|
103
111
|
if (accept.includes('application/json'))
|
|
104
|
-
return
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (userAgent.includes(
|
|
108
|
-
return
|
|
109
|
-
|
|
110
|
-
return true;
|
|
111
|
-
if (userAgent.includes('python'))
|
|
112
|
-
return true;
|
|
113
|
-
if (userAgent.includes('node'))
|
|
114
|
-
return true;
|
|
115
|
-
if (userAgent.includes('axios'))
|
|
116
|
-
return true;
|
|
117
|
-
if (userAgent.includes('fetch'))
|
|
118
|
-
return true;
|
|
119
|
-
if (userAgent.includes('bot'))
|
|
120
|
-
return true;
|
|
121
|
-
if (userAgent.includes('anthropic'))
|
|
122
|
-
return true;
|
|
123
|
-
if (userAgent.includes('openai'))
|
|
124
|
-
return true;
|
|
125
|
-
if (userAgent.includes('claude'))
|
|
126
|
-
return true;
|
|
127
|
-
if (userAgent.includes('gpt'))
|
|
128
|
-
return true;
|
|
129
|
-
// If no user agent at all, probably a bot
|
|
112
|
+
return 'json';
|
|
113
|
+
// Known bot user agents → JSON
|
|
114
|
+
const botSignals = ['curl', 'httpie', 'wget', 'python', 'node', 'axios', 'fetch', 'bot', 'anthropic', 'openai', 'claude', 'gpt'];
|
|
115
|
+
if (botSignals.some(s => userAgent.includes(s)))
|
|
116
|
+
return 'json';
|
|
117
|
+
// No user agent at all → probably a bot
|
|
130
118
|
if (!userAgent)
|
|
131
|
-
return
|
|
132
|
-
|
|
119
|
+
return 'json';
|
|
120
|
+
// Default: human browser → HTML
|
|
121
|
+
return 'html';
|
|
133
122
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
123
|
+
app.get('/', async (c) => {
|
|
124
|
+
const version = c.env.BOTCHA_VERSION || '0.11.0';
|
|
125
|
+
const preference = detectAcceptPreference(c);
|
|
126
|
+
const baseUrl = new URL(c.req.url).origin;
|
|
127
|
+
// Check if agent is verified (optional Bearer token)
|
|
128
|
+
const authHeader = c.req.header('authorization');
|
|
129
|
+
const token = extractBearerToken(authHeader);
|
|
130
|
+
let isVerified = false;
|
|
131
|
+
let tokenPayload;
|
|
132
|
+
if (token) {
|
|
133
|
+
const result = await verifyToken(token, c.env.JWT_SECRET, c.env);
|
|
134
|
+
if (result.valid) {
|
|
135
|
+
isVerified = true;
|
|
136
|
+
tokenPayload = result.payload;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// HTML: landing page for human browsers
|
|
140
|
+
if (preference === 'html') {
|
|
141
|
+
// Check for visitor cookie (set by POST /gate after human enters agent's token)
|
|
142
|
+
const cookieHeader = c.req.header('cookie') || '';
|
|
143
|
+
const visitorToken = cookieHeader.split(';').find(ck => ck.trim().startsWith('botcha_visitor='))?.split('=')[1]?.trim();
|
|
144
|
+
if (visitorToken) {
|
|
145
|
+
// Verify visitor cookie — accepts both botcha-verified (legacy) and botcha-visitor (long-lived)
|
|
146
|
+
try {
|
|
147
|
+
const { payload: vPayload } = await jwtVerify(visitorToken, new TextEncoder().encode(c.env.JWT_SECRET), { algorithms: ['HS256'] });
|
|
148
|
+
if (vPayload.type === 'botcha-visitor' || vPayload.type === 'botcha-verified') {
|
|
149
|
+
return c.html(_jsx(VerifiedLandingPage, { version: version, solveTime: vPayload.solveTime }));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Invalid/expired cookie — fall through to unverified landing
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return c.html(_jsx(LandingPage, { version: version }));
|
|
157
|
+
}
|
|
158
|
+
// === UNVERIFIED: minimal teaser — just enough to get started ===
|
|
159
|
+
if (!isVerified) {
|
|
160
|
+
const teaser = {
|
|
161
|
+
name: 'BOTCHA',
|
|
162
|
+
version,
|
|
163
|
+
tagline: 'Prove you are a bot. Humans need not apply.',
|
|
164
|
+
status: 'unverified',
|
|
165
|
+
message: 'Verify yourself to see the full API documentation.',
|
|
166
|
+
verify: {
|
|
167
|
+
step1: `GET ${baseUrl}/v1/token — receive a speed challenge (5 SHA-256 hashes)`,
|
|
168
|
+
step2: 'Solve: compute SHA-256 of each number, return first 8 hex chars',
|
|
169
|
+
step3: `POST ${baseUrl}/v1/token/verify {"id":"<challenge_id>","answers":["hash1","hash2",...]}`,
|
|
170
|
+
step4: `Revisit: GET ${baseUrl}/ with header Authorization: Bearer <access_token>`,
|
|
171
|
+
},
|
|
172
|
+
human_handoff: {
|
|
173
|
+
note: 'After verifying, you will receive a short code (BOTCHA-XXXXXX). Give it to your human.',
|
|
174
|
+
instruction: 'Tell your human: "Enter this code at botcha.ai to unlock the site."',
|
|
175
|
+
code_field: 'human_code from step 3 response',
|
|
176
|
+
},
|
|
177
|
+
hint: 'You have 500ms. Only a bot can do this.',
|
|
178
|
+
links: {
|
|
179
|
+
openapi: `${baseUrl}/openapi.json`,
|
|
180
|
+
ai_txt: `${baseUrl}/ai.txt`,
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
if (preference === 'markdown') {
|
|
184
|
+
const md = `# BOTCHA — Reverse CAPTCHA for AI Agents
|
|
185
|
+
|
|
186
|
+
> Prove you are a bot. Humans need not apply.
|
|
187
|
+
|
|
188
|
+
**Status:** Unverified. Solve a challenge to see the full documentation.
|
|
189
|
+
|
|
190
|
+
## Verify Yourself
|
|
191
|
+
|
|
192
|
+
1. \`GET ${baseUrl}/v1/token\` — receive a speed challenge (5 SHA-256 hashes)
|
|
193
|
+
2. Solve: compute SHA-256 of each number, return first 8 hex chars
|
|
194
|
+
3. \`POST ${baseUrl}/v1/token/verify {"id":"<challenge_id>","answers":["hash1","hash2",...]}\`
|
|
195
|
+
4. Revisit: \`GET ${baseUrl}/\` with header \`Authorization: Bearer <access_token>\`
|
|
196
|
+
|
|
197
|
+
You have 500ms. Only a bot can do this.
|
|
198
|
+
|
|
199
|
+
## Human Handoff
|
|
200
|
+
|
|
201
|
+
After verifying, give your human the \`access_token\` and tell them:
|
|
202
|
+
**"Paste this token into the input at botcha.ai to unlock the full site."**
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
*Full API docs, SDKs, onboarding, and endpoint reference available after verification.*
|
|
172
207
|
`;
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
return c.
|
|
179
|
-
|
|
208
|
+
return c.body(md, 200, {
|
|
209
|
+
'Content-Type': 'text/markdown; charset=utf-8',
|
|
210
|
+
'Vary': 'Accept, Authorization',
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return c.json(teaser, 200, { 'Vary': 'Accept, Authorization' });
|
|
214
|
+
}
|
|
215
|
+
// === VERIFIED: full documentation ===
|
|
216
|
+
if (preference === 'markdown') {
|
|
217
|
+
const markdown = getBotchaMarkdown(version);
|
|
218
|
+
return c.body(markdown, 200, {
|
|
219
|
+
'Content-Type': 'text/markdown; charset=utf-8',
|
|
220
|
+
'Vary': 'Accept, Authorization',
|
|
221
|
+
'Cache-Control': 'public, max-age=3600',
|
|
180
222
|
});
|
|
181
223
|
}
|
|
182
|
-
// For bots/agents, return comprehensive JSON documentation
|
|
183
224
|
return c.json({
|
|
184
225
|
name: 'BOTCHA',
|
|
185
226
|
version,
|
|
186
|
-
runtime: 'cloudflare-workers',
|
|
187
227
|
tagline: 'Prove you are a bot. Humans need not apply.',
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
step5: 'You are now registered. Use app_id with all challenge/token endpoints (?app_id=...).',
|
|
197
|
-
step6_dashboard: 'To give your human dashboard access: POST /v1/auth/device-code with {"app_id": "..."}, solve the speed challenge, then give your human the BOTCHA-XXXX code to enter at /dashboard/code',
|
|
198
|
-
step7_recovery: 'If you lose your app_secret, POST /v1/auth/recover with {"email": "..."} — a recovery device code is sent to the verified email.',
|
|
228
|
+
status: 'verified',
|
|
229
|
+
description: 'Reverse CAPTCHA for AI agents. Computational challenges only bots can solve. Identity layer for the agentic web.',
|
|
230
|
+
// Echo back your identity
|
|
231
|
+
your_identity: {
|
|
232
|
+
token_type: tokenPayload?.type,
|
|
233
|
+
app_id: tokenPayload?.app_id || null,
|
|
234
|
+
audience: tokenPayload?.aud || null,
|
|
235
|
+
solve_time_ms: tokenPayload?.solveTime,
|
|
199
236
|
},
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
237
|
+
// === FULL SETUP: register your app and agent identity ===
|
|
238
|
+
onboarding: {
|
|
239
|
+
note: 'For production use. Creates your app, verifies your human, registers your identity.',
|
|
240
|
+
steps: [
|
|
241
|
+
'1. Ask your human operator for their email address.',
|
|
242
|
+
`2. POST ${baseUrl}/v1/apps {"email":"human@example.com"} — returns app_id + app_secret (shown ONCE).`,
|
|
243
|
+
'3. Human receives a 6-digit verification code via email — ask them for it.',
|
|
244
|
+
`4. POST ${baseUrl}/v1/apps/{app_id}/verify-email {"code":"123456"} — enables account recovery.`,
|
|
245
|
+
`5. Register yourself: POST ${baseUrl}/v1/agents/register {"name":"my-agent","operator":"my-org"} — persistent agent identity.`,
|
|
246
|
+
`6. Solve challenges with your app: GET ${baseUrl}/v1/challenges?app_id=...`,
|
|
247
|
+
`7. Dashboard for your human: POST ${baseUrl}/v1/auth/device-code {"app_id":"..."}, solve challenge, give human the BOTCHA-XXXX code for /dashboard/code.`,
|
|
248
|
+
`8. Lost your secret? POST ${baseUrl}/v1/auth/recover {"email":"..."} — recovery code emailed.`,
|
|
249
|
+
],
|
|
208
250
|
},
|
|
251
|
+
// === All endpoints, grouped by domain ===
|
|
209
252
|
endpoints: {
|
|
210
253
|
challenges: {
|
|
211
|
-
'GET /v1/challenges': 'Get
|
|
212
|
-
'GET /v1/challenges?type=speed': '
|
|
213
|
-
'GET /v1/challenges?type=standard': '
|
|
254
|
+
'GET /v1/challenges': 'Get a challenge (hybrid by default, no auth required)',
|
|
255
|
+
'GET /v1/challenges?type=speed': 'Speed-only (SHA256 in <500ms)',
|
|
256
|
+
'GET /v1/challenges?type=standard': 'Standard puzzle challenge',
|
|
214
257
|
'POST /v1/challenges/:id/verify': 'Verify challenge solution',
|
|
215
258
|
},
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
'
|
|
219
|
-
'
|
|
220
|
-
'POST /v1/
|
|
221
|
-
|
|
222
|
-
streaming: {
|
|
223
|
-
'GET /v1/challenge/stream': 'SSE streaming challenge (interactive, real-time)',
|
|
224
|
-
'POST /v1/challenge/stream/:session': 'Send actions to streaming session',
|
|
259
|
+
tokens: {
|
|
260
|
+
note: 'Use token flow when you need a Bearer token for protected endpoints.',
|
|
261
|
+
'GET /v1/token': 'Get speed challenge for token flow (?audience= optional)',
|
|
262
|
+
'POST /v1/token/verify': 'Submit solution → access_token (5min) + refresh_token (1hr)',
|
|
263
|
+
'POST /v1/token/refresh': 'Refresh access token',
|
|
264
|
+
'POST /v1/token/revoke': 'Revoke a token',
|
|
225
265
|
},
|
|
226
|
-
|
|
227
|
-
'GET /
|
|
228
|
-
'
|
|
229
|
-
'POST /v1/token/refresh': 'Refresh access token using refresh token',
|
|
230
|
-
'POST /v1/token/revoke': 'Revoke a token (access or refresh)',
|
|
231
|
-
'GET /agent-only': 'Protected endpoint (requires Bearer token)',
|
|
266
|
+
protected: {
|
|
267
|
+
'GET /agent-only': 'Demo protected endpoint — requires Bearer token',
|
|
268
|
+
'GET /': 'This documentation (requires Bearer token for full version)',
|
|
232
269
|
},
|
|
233
270
|
apps: {
|
|
234
|
-
|
|
235
|
-
'
|
|
271
|
+
note: 'Create an app for isolated rate limits, scoped tokens, and dashboard access.',
|
|
272
|
+
'POST /v1/apps': 'Create app (email required) → app_id + app_secret',
|
|
273
|
+
'GET /v1/apps/:id': 'Get app info',
|
|
236
274
|
'POST /v1/apps/:id/verify-email': 'Verify email with 6-digit code',
|
|
237
|
-
'POST /v1/apps/:id/resend-verification': 'Resend verification email',
|
|
238
275
|
'POST /v1/apps/:id/rotate-secret': 'Rotate app secret (auth required)',
|
|
239
276
|
},
|
|
277
|
+
agents: {
|
|
278
|
+
note: 'Register a persistent identity for your agent.',
|
|
279
|
+
'POST /v1/agents/register': 'Register agent identity (name, operator, version)',
|
|
280
|
+
'GET /v1/agents/:id': 'Get agent by ID (public, no auth)',
|
|
281
|
+
'GET /v1/agents': 'List all agents for your app (auth required)',
|
|
282
|
+
},
|
|
240
283
|
recovery: {
|
|
241
|
-
'POST /v1/auth/recover': '
|
|
284
|
+
'POST /v1/auth/recover': 'Account recovery via verified email',
|
|
242
285
|
},
|
|
243
286
|
dashboard: {
|
|
244
|
-
'
|
|
245
|
-
'GET /dashboard
|
|
246
|
-
'GET /dashboard/code': 'Enter device code (human-facing)',
|
|
247
|
-
'GET /dashboard/api/*': 'htmx data fragments (overview, volume, types, performance, errors, geo)',
|
|
248
|
-
'POST /v1/auth/dashboard': 'Request challenge for dashboard login (agent-first)',
|
|
249
|
-
'POST /v1/auth/dashboard/verify': 'Solve challenge, get session token',
|
|
250
|
-
'POST /v1/auth/device-code': 'Request challenge for device code flow',
|
|
251
|
-
'POST /v1/auth/device-code/verify': 'Solve challenge, get device code (BOTCHA-XXXX)',
|
|
252
|
-
},
|
|
253
|
-
badges: {
|
|
254
|
-
'GET /badge/:id': 'Badge verification page (HTML)',
|
|
255
|
-
'GET /badge/:id/image': 'Badge image (SVG)',
|
|
256
|
-
'GET /api/badge/:id': 'Badge verification (JSON)',
|
|
257
|
-
},
|
|
258
|
-
info: {
|
|
259
|
-
'GET /': 'This documentation (JSON for bots, ASCII for humans)',
|
|
260
|
-
'GET /health': 'Health check endpoint',
|
|
287
|
+
'POST /v1/auth/device-code': 'Get challenge for device code flow',
|
|
288
|
+
'GET /dashboard': 'Metrics dashboard (login required)',
|
|
261
289
|
},
|
|
262
290
|
},
|
|
291
|
+
// === Reference ===
|
|
263
292
|
challengeTypes: {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
timeLimit: '500ms base + network latency compensation',
|
|
268
|
-
rttAware: 'Include ?ts=<timestamp> for fair timeout adjustment',
|
|
269
|
-
formula: 'timeout = 500ms + (2 × RTT) + 100ms buffer',
|
|
270
|
-
},
|
|
271
|
-
reasoning: {
|
|
272
|
-
description: 'Answer 3 questions requiring AI reasoning capabilities',
|
|
273
|
-
difficulty: 'Requires LLM-level comprehension',
|
|
274
|
-
timeLimit: '30s',
|
|
275
|
-
},
|
|
276
|
-
hybrid: {
|
|
277
|
-
description: 'Combines speed AND reasoning challenges',
|
|
278
|
-
difficulty: 'The ultimate bot verification',
|
|
279
|
-
timeLimit: 'Speed: RTT-aware, Reasoning: 30s',
|
|
280
|
-
rttAware: 'Speed component automatically adjusts for network latency',
|
|
281
|
-
},
|
|
282
|
-
},
|
|
283
|
-
authentication: {
|
|
284
|
-
flow: [
|
|
285
|
-
'1. GET /v1/token?audience=myapi - receive challenge (optional audience param)',
|
|
286
|
-
'2. Solve the challenge',
|
|
287
|
-
'3. POST /v1/token/verify - submit solution with optional audience and bind_ip',
|
|
288
|
-
'4. Receive access_token (5 min) and refresh_token (1 hour)',
|
|
289
|
-
'5. Use: Authorization: Bearer <access_token>',
|
|
290
|
-
'6. Refresh: POST /v1/token/refresh with refresh_token',
|
|
291
|
-
'7. Revoke: POST /v1/token/revoke with token',
|
|
292
|
-
],
|
|
293
|
-
tokens: {
|
|
294
|
-
access_token: '5 minutes (for API access)',
|
|
295
|
-
refresh_token: '1 hour (to get new access tokens)',
|
|
296
|
-
},
|
|
297
|
-
usage: 'Authorization: Bearer <access_token>',
|
|
298
|
-
features: ['audience claims', 'client IP binding', 'token revocation', 'refresh tokens'],
|
|
299
|
-
},
|
|
300
|
-
rttAwareness: {
|
|
301
|
-
purpose: 'Fair challenges for agents on slow networks',
|
|
302
|
-
usage: 'Include client timestamp in ?ts=<timestamp_ms> or X-Client-Timestamp header',
|
|
303
|
-
formula: 'timeout = 500ms + (2 × RTT) + 100ms buffer',
|
|
304
|
-
example: '/v1/challenges?type=speed&ts=1770722465000',
|
|
305
|
-
benefit: 'Agents worldwide get fair treatment regardless of network speed',
|
|
306
|
-
security: 'Humans still cannot solve challenges even with extra time',
|
|
293
|
+
hybrid: 'Speed + reasoning combined. The default. Proves you can compute AND think.',
|
|
294
|
+
speed: 'SHA256 hashes in <500ms. RTT-aware: include ?ts=<timestamp> for fair timeout.',
|
|
295
|
+
reasoning: '3 LLM-level questions in 30s. Only AI can parse these.',
|
|
307
296
|
},
|
|
308
297
|
rateLimit: {
|
|
309
298
|
free: '100 challenges/hour/IP',
|
|
@@ -312,26 +301,82 @@ app.get('/', (c) => {
|
|
|
312
301
|
sdk: {
|
|
313
302
|
npm: 'npm install @dupecom/botcha',
|
|
314
303
|
python: 'pip install botcha',
|
|
315
|
-
cloudflare: 'npm install @dupecom/botcha-cloudflare',
|
|
316
304
|
verify_ts: 'npm install @botcha/verify',
|
|
317
305
|
verify_python: 'pip install botcha-verify',
|
|
318
|
-
usage: "import { BotchaClient } from '@dupecom/botcha/client'",
|
|
319
306
|
},
|
|
320
307
|
links: {
|
|
308
|
+
openapi: `${baseUrl}/openapi.json`,
|
|
309
|
+
ai_txt: `${baseUrl}/ai.txt`,
|
|
321
310
|
github: 'https://github.com/dupe-com/botcha',
|
|
322
311
|
npm: 'https://www.npmjs.com/package/@dupecom/botcha',
|
|
323
312
|
pypi: 'https://pypi.org/project/botcha',
|
|
324
|
-
npmCloudflare: 'https://www.npmjs.com/package/@dupecom/botcha-cloudflare',
|
|
325
|
-
openapi: 'https://botcha.ai/openapi.json',
|
|
326
|
-
aiPlugin: 'https://botcha.ai/.well-known/ai-plugin.json',
|
|
327
313
|
},
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
314
|
+
content_negotiation: {
|
|
315
|
+
note: 'This endpoint supports content negotiation via the Accept header.',
|
|
316
|
+
'text/markdown': 'Token-efficient Markdown documentation (best for LLMs)',
|
|
317
|
+
'application/json': 'Structured JSON documentation (this response)',
|
|
318
|
+
'text/html': 'HTML landing page (for browsers)',
|
|
332
319
|
},
|
|
320
|
+
}, 200, {
|
|
321
|
+
'Vary': 'Accept, Authorization',
|
|
333
322
|
});
|
|
334
323
|
});
|
|
324
|
+
// POST /gate — human enters short code (BOTCHA-XXXXXX) from their agent
|
|
325
|
+
// The code maps to a JWT in KV. This structural separation means agents can't skip the handoff.
|
|
326
|
+
app.post('/gate', async (c) => {
|
|
327
|
+
const version = c.env.BOTCHA_VERSION || '0.11.0';
|
|
328
|
+
const body = await c.req.parseBody();
|
|
329
|
+
const input = (body['code'] || '').trim().toUpperCase();
|
|
330
|
+
if (!input) {
|
|
331
|
+
return c.html(_jsx(LandingPage, { version: version, error: "Enter the code your agent gave you." }), 400);
|
|
332
|
+
}
|
|
333
|
+
// Normalize: accept "BOTCHA-ABC123" or just "ABC123"
|
|
334
|
+
const code = input.startsWith('BOTCHA-') ? input : `BOTCHA-${input}`;
|
|
335
|
+
// Look up the code in KV
|
|
336
|
+
let token = null;
|
|
337
|
+
try {
|
|
338
|
+
token = await c.env.CHALLENGES.get(`gate:${code}`);
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
// KV error — fail open with helpful message
|
|
342
|
+
}
|
|
343
|
+
if (!token) {
|
|
344
|
+
return c.html(_jsx(LandingPage, { version: version, error: "Invalid or expired code. Ask your agent to solve a new challenge." }), 401);
|
|
345
|
+
}
|
|
346
|
+
// Verify the token is still valid
|
|
347
|
+
const result = await verifyToken(token, c.env.JWT_SECRET, c.env);
|
|
348
|
+
if (!result.valid) {
|
|
349
|
+
// Code existed but token expired — clean up
|
|
350
|
+
try {
|
|
351
|
+
await c.env.CHALLENGES.delete(`gate:${code}`);
|
|
352
|
+
}
|
|
353
|
+
catch { }
|
|
354
|
+
return c.html(_jsx(LandingPage, { version: version, error: "Code expired. Ask your agent to solve a new challenge." }), 401);
|
|
355
|
+
}
|
|
356
|
+
// Delete the code after use (one-time use)
|
|
357
|
+
try {
|
|
358
|
+
await c.env.CHALLENGES.delete(`gate:${code}`);
|
|
359
|
+
}
|
|
360
|
+
catch { }
|
|
361
|
+
// Mint a long-lived visitor JWT (1 year) — only proves "an agent vouched for this human"
|
|
362
|
+
// This is NOT an access token and cannot be used for API calls
|
|
363
|
+
const vPayload = result.payload;
|
|
364
|
+
const visitorToken = await new SignJWT({
|
|
365
|
+
type: 'botcha-visitor',
|
|
366
|
+
solveTime: vPayload?.solveTime,
|
|
367
|
+
gateCode: code,
|
|
368
|
+
})
|
|
369
|
+
.setProtectedHeader({ alg: 'HS256' })
|
|
370
|
+
.setIssuedAt()
|
|
371
|
+
.setExpirationTime('365d')
|
|
372
|
+
.sign(new TextEncoder().encode(c.env.JWT_SECRET));
|
|
373
|
+
// Set a 1-year visitor cookie and redirect to /
|
|
374
|
+
const ONE_YEAR = 365 * 24 * 60 * 60;
|
|
375
|
+
const headers = new Headers();
|
|
376
|
+
headers.append('Set-Cookie', `botcha_visitor=${visitorToken}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${ONE_YEAR}`);
|
|
377
|
+
headers.set('Location', '/');
|
|
378
|
+
return new Response(null, { status: 302, headers });
|
|
379
|
+
});
|
|
335
380
|
app.get('/health', (c) => {
|
|
336
381
|
return c.json({ status: 'ok', runtime: 'cloudflare-workers' });
|
|
337
382
|
});
|
|
@@ -352,7 +397,7 @@ app.get('/ai.txt', (c) => {
|
|
|
352
397
|
});
|
|
353
398
|
// OpenAPI spec
|
|
354
399
|
app.get('/openapi.json', (c) => {
|
|
355
|
-
const version = c.env.BOTCHA_VERSION || '0.
|
|
400
|
+
const version = c.env.BOTCHA_VERSION || '0.11.0';
|
|
356
401
|
return c.json(getOpenApiSpec(version), 200, {
|
|
357
402
|
'Cache-Control': 'public, max-age=3600',
|
|
358
403
|
});
|
|
@@ -623,24 +668,46 @@ app.post('/v1/token/verify', async (c) => {
|
|
|
623
668
|
// Get badge information (for backward compatibility)
|
|
624
669
|
const baseUrl = new URL(c.req.url).origin;
|
|
625
670
|
const badge = await createBadgeResponse('speed-challenge', c.env.JWT_SECRET, baseUrl, result.solveTimeMs);
|
|
671
|
+
// Generate short human-readable gate code (BOTCHA-XXXX)
|
|
672
|
+
// This is structurally separate from the JWT — agents use the token, humans use the code
|
|
673
|
+
const gateChars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // no 0/O/1/I confusion
|
|
674
|
+
let gateCode = 'BOTCHA-';
|
|
675
|
+
for (let i = 0; i < 6; i++)
|
|
676
|
+
gateCode += gateChars[Math.floor(Math.random() * gateChars.length)];
|
|
677
|
+
// Store code → token mapping in KV (5-min TTL, same as access_token)
|
|
678
|
+
try {
|
|
679
|
+
await c.env.CHALLENGES.put(`gate:${gateCode}`, tokenResult.access_token, { expirationTtl: 300 });
|
|
680
|
+
}
|
|
681
|
+
catch {
|
|
682
|
+
// Fail-open: if KV fails, agent can still use the JWT directly
|
|
683
|
+
}
|
|
626
684
|
return c.json({
|
|
685
|
+
// === Essential fields (what you need) ===
|
|
686
|
+
success: true,
|
|
627
687
|
verified: true,
|
|
628
688
|
access_token: tokenResult.access_token,
|
|
629
689
|
expires_in: tokenResult.expires_in,
|
|
630
690
|
refresh_token: tokenResult.refresh_token,
|
|
631
691
|
refresh_expires_in: tokenResult.refresh_expires_in,
|
|
632
692
|
solveTimeMs: result.solveTimeMs,
|
|
633
|
-
|
|
634
|
-
//
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
693
|
+
message: `Challenge verified in ${result.solveTimeMs}ms. You are a bot.`,
|
|
694
|
+
// === Human handoff code (GIVE THIS TO YOUR HUMAN) ===
|
|
695
|
+
human_code: gateCode,
|
|
696
|
+
human_url: `${baseUrl}`,
|
|
697
|
+
human_instruction: `Give your human this code: ${gateCode} — they enter it at ${baseUrl} to see the site.`,
|
|
698
|
+
// === What to do next ===
|
|
639
699
|
usage: {
|
|
640
|
-
header: 'Authorization: Bearer <
|
|
641
|
-
|
|
642
|
-
|
|
700
|
+
header: 'Authorization: Bearer <access_token>',
|
|
701
|
+
try_it: 'GET /agent-only',
|
|
702
|
+
full_docs: 'GET / with Authorization: Bearer <access_token>',
|
|
703
|
+
human_handoff: `Tell your human: "Enter code ${gateCode} at ${baseUrl} to unlock the site."`,
|
|
704
|
+
refresh: 'POST /v1/token/refresh with {"refresh_token":"<refresh_token>"}',
|
|
705
|
+
revoke: 'POST /v1/token/revoke with {"token":"<token>"}',
|
|
643
706
|
},
|
|
707
|
+
// === Badge (shareable proof of verification) ===
|
|
708
|
+
badge,
|
|
709
|
+
// Backward compatibility
|
|
710
|
+
token: tokenResult.access_token,
|
|
644
711
|
});
|
|
645
712
|
});
|
|
646
713
|
// Refresh access token using refresh token
|
|
@@ -995,11 +1062,13 @@ app.get('/agent-only', async (c) => {
|
|
|
995
1062
|
if (!token) {
|
|
996
1063
|
return c.json({
|
|
997
1064
|
error: 'UNAUTHORIZED',
|
|
998
|
-
message: '
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1065
|
+
message: 'This endpoint requires BOTCHA verification. Get a token first.',
|
|
1066
|
+
how_to_verify: {
|
|
1067
|
+
step1: 'GET /v1/token — receive a speed challenge',
|
|
1068
|
+
step2: 'POST /v1/token/verify with {id, answers} — receive access_token',
|
|
1069
|
+
step3: 'Retry this request with header: Authorization: Bearer <access_token>',
|
|
1070
|
+
},
|
|
1071
|
+
alternative: 'Or use X-Botcha-Landing-Token header (from embedded HTML challenges)',
|
|
1003
1072
|
}, 401);
|
|
1004
1073
|
}
|
|
1005
1074
|
const result = await verifyToken(token, c.env.JWT_SECRET, c.env);
|
|
@@ -1011,16 +1080,35 @@ app.get('/agent-only', async (c) => {
|
|
|
1011
1080
|
message: result.error || 'Token is invalid or expired',
|
|
1012
1081
|
}, 401);
|
|
1013
1082
|
}
|
|
1014
|
-
// JWT verified
|
|
1083
|
+
// JWT verified — echo back rich identity info as a prove-and-access demo
|
|
1084
|
+
const payload = result.payload;
|
|
1015
1085
|
return c.json({
|
|
1016
1086
|
success: true,
|
|
1017
|
-
message: '
|
|
1087
|
+
message: 'Welcome, verified agent. This resource is only accessible to BOTCHA-verified bots.',
|
|
1018
1088
|
verified: true,
|
|
1019
|
-
agent: 'jwt-verified',
|
|
1020
1089
|
method: 'bearer-token',
|
|
1021
1090
|
timestamp: new Date().toISOString(),
|
|
1022
|
-
|
|
1023
|
-
|
|
1091
|
+
// Echo back what the token proves about you
|
|
1092
|
+
identity: {
|
|
1093
|
+
token_type: payload?.type,
|
|
1094
|
+
app_id: payload?.app_id || null,
|
|
1095
|
+
audience: payload?.aud || null,
|
|
1096
|
+
client_ip: payload?.client_ip || null,
|
|
1097
|
+
solve_time_ms: payload?.solveTime,
|
|
1098
|
+
issued_at: payload?.iat ? new Date(payload.iat * 1000).toISOString() : null,
|
|
1099
|
+
expires_at: payload?.exp ? new Date(payload.exp * 1000).toISOString() : null,
|
|
1100
|
+
},
|
|
1101
|
+
// Show what you can do now that you're verified
|
|
1102
|
+
capabilities: {
|
|
1103
|
+
description: 'As a verified agent, you can access any BOTCHA-protected API.',
|
|
1104
|
+
next_steps: [
|
|
1105
|
+
'Register your agent identity: POST /v1/agents/register',
|
|
1106
|
+
'Access any service that uses @botcha/verify middleware',
|
|
1107
|
+
'Refresh your token: POST /v1/token/refresh',
|
|
1108
|
+
'Give your human dashboard access: POST /v1/auth/device-code',
|
|
1109
|
+
],
|
|
1110
|
+
},
|
|
1111
|
+
secret: 'The humans will never see this. Their fingers are too slow.',
|
|
1024
1112
|
});
|
|
1025
1113
|
});
|
|
1026
1114
|
// ============ BADGE ENDPOINTS ============
|
|
@@ -1363,6 +1451,167 @@ app.post('/v1/apps/:id/rotate-secret', async (c) => {
|
|
|
1363
1451
|
warning: '⚠️ Save your new app_secret now — it cannot be retrieved again! The old secret is now invalid.',
|
|
1364
1452
|
});
|
|
1365
1453
|
});
|
|
1454
|
+
// ============ AGENT REGISTRY API ============
|
|
1455
|
+
// Register a new agent
|
|
1456
|
+
app.post('/v1/agents/register', async (c) => {
|
|
1457
|
+
try {
|
|
1458
|
+
// Extract app_id from query param or JWT
|
|
1459
|
+
const queryAppId = c.req.query('app_id');
|
|
1460
|
+
// Try to get from JWT Bearer token
|
|
1461
|
+
let jwtAppId;
|
|
1462
|
+
const authHeader = c.req.header('authorization');
|
|
1463
|
+
const token = extractBearerToken(authHeader);
|
|
1464
|
+
if (token) {
|
|
1465
|
+
const result = await verifyToken(token, c.env.JWT_SECRET, c.env);
|
|
1466
|
+
if (result.valid && result.payload) {
|
|
1467
|
+
jwtAppId = result.payload.app_id;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
const app_id = queryAppId || jwtAppId;
|
|
1471
|
+
if (!app_id) {
|
|
1472
|
+
return c.json({
|
|
1473
|
+
success: false,
|
|
1474
|
+
error: 'MISSING_APP_ID',
|
|
1475
|
+
message: 'app_id is required. Provide it as a query parameter (?app_id=...) or in the JWT token.',
|
|
1476
|
+
}, 401);
|
|
1477
|
+
}
|
|
1478
|
+
// Validate app_id exists
|
|
1479
|
+
const validation = await validateAppId(app_id, c.env.APPS);
|
|
1480
|
+
if (!validation.valid) {
|
|
1481
|
+
return c.json({
|
|
1482
|
+
success: false,
|
|
1483
|
+
error: 'INVALID_APP_ID',
|
|
1484
|
+
message: validation.error || 'Invalid app_id',
|
|
1485
|
+
}, 400);
|
|
1486
|
+
}
|
|
1487
|
+
// Parse request body
|
|
1488
|
+
const body = await c.req.json().catch(() => ({}));
|
|
1489
|
+
const { name, operator, version } = body;
|
|
1490
|
+
if (!name || typeof name !== 'string') {
|
|
1491
|
+
return c.json({
|
|
1492
|
+
success: false,
|
|
1493
|
+
error: 'MISSING_NAME',
|
|
1494
|
+
message: 'Agent name is required. Provide { "name": "Your Agent Name" } in the request body.',
|
|
1495
|
+
}, 400);
|
|
1496
|
+
}
|
|
1497
|
+
// Create the agent
|
|
1498
|
+
const agent = await createAgent(c.env.AGENTS, app_id, { name, operator, version });
|
|
1499
|
+
if (!agent) {
|
|
1500
|
+
return c.json({
|
|
1501
|
+
success: false,
|
|
1502
|
+
error: 'AGENT_CREATION_FAILED',
|
|
1503
|
+
message: 'Failed to create agent. Please try again.',
|
|
1504
|
+
}, 500);
|
|
1505
|
+
}
|
|
1506
|
+
return c.json({
|
|
1507
|
+
success: true,
|
|
1508
|
+
agent_id: agent.agent_id,
|
|
1509
|
+
app_id: agent.app_id,
|
|
1510
|
+
name: agent.name,
|
|
1511
|
+
operator: agent.operator,
|
|
1512
|
+
version: agent.version,
|
|
1513
|
+
created_at: new Date(agent.created_at).toISOString(),
|
|
1514
|
+
}, 201);
|
|
1515
|
+
}
|
|
1516
|
+
catch (error) {
|
|
1517
|
+
return c.json({
|
|
1518
|
+
success: false,
|
|
1519
|
+
error: 'INTERNAL_ERROR',
|
|
1520
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
1521
|
+
}, 500);
|
|
1522
|
+
}
|
|
1523
|
+
});
|
|
1524
|
+
// Get agent by ID
|
|
1525
|
+
app.get('/v1/agents/:id', async (c) => {
|
|
1526
|
+
try {
|
|
1527
|
+
const agent_id = c.req.param('id');
|
|
1528
|
+
if (!agent_id) {
|
|
1529
|
+
return c.json({
|
|
1530
|
+
success: false,
|
|
1531
|
+
error: 'MISSING_AGENT_ID',
|
|
1532
|
+
message: 'Agent ID is required',
|
|
1533
|
+
}, 400);
|
|
1534
|
+
}
|
|
1535
|
+
const agent = await getAgent(c.env.AGENTS, agent_id);
|
|
1536
|
+
if (!agent) {
|
|
1537
|
+
return c.json({
|
|
1538
|
+
success: false,
|
|
1539
|
+
error: 'AGENT_NOT_FOUND',
|
|
1540
|
+
message: `No agent found with ID: ${agent_id}`,
|
|
1541
|
+
}, 404);
|
|
1542
|
+
}
|
|
1543
|
+
return c.json({
|
|
1544
|
+
success: true,
|
|
1545
|
+
agent_id: agent.agent_id,
|
|
1546
|
+
app_id: agent.app_id,
|
|
1547
|
+
name: agent.name,
|
|
1548
|
+
operator: agent.operator,
|
|
1549
|
+
version: agent.version,
|
|
1550
|
+
created_at: new Date(agent.created_at).toISOString(),
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
catch (error) {
|
|
1554
|
+
return c.json({
|
|
1555
|
+
success: false,
|
|
1556
|
+
error: 'INTERNAL_ERROR',
|
|
1557
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
1558
|
+
}, 500);
|
|
1559
|
+
}
|
|
1560
|
+
});
|
|
1561
|
+
// List all agents for an app
|
|
1562
|
+
app.get('/v1/agents', async (c) => {
|
|
1563
|
+
try {
|
|
1564
|
+
// Extract app_id from query param or JWT
|
|
1565
|
+
const queryAppId = c.req.query('app_id');
|
|
1566
|
+
// Try to get from JWT Bearer token
|
|
1567
|
+
let jwtAppId;
|
|
1568
|
+
const authHeader = c.req.header('authorization');
|
|
1569
|
+
const token = extractBearerToken(authHeader);
|
|
1570
|
+
if (token) {
|
|
1571
|
+
const result = await verifyToken(token, c.env.JWT_SECRET, c.env);
|
|
1572
|
+
if (result.valid && result.payload) {
|
|
1573
|
+
jwtAppId = result.payload.app_id;
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
const app_id = queryAppId || jwtAppId;
|
|
1577
|
+
if (!app_id) {
|
|
1578
|
+
return c.json({
|
|
1579
|
+
success: false,
|
|
1580
|
+
error: 'MISSING_APP_ID',
|
|
1581
|
+
message: 'app_id is required. Provide it as a query parameter (?app_id=...) or in the JWT token.',
|
|
1582
|
+
}, 401);
|
|
1583
|
+
}
|
|
1584
|
+
// Validate app_id exists
|
|
1585
|
+
const validation = await validateAppId(app_id, c.env.APPS);
|
|
1586
|
+
if (!validation.valid) {
|
|
1587
|
+
return c.json({
|
|
1588
|
+
success: false,
|
|
1589
|
+
error: 'INVALID_APP_ID',
|
|
1590
|
+
message: validation.error || 'Invalid app_id',
|
|
1591
|
+
}, 400);
|
|
1592
|
+
}
|
|
1593
|
+
// Get all agents for this app
|
|
1594
|
+
const agents = await listAgents(c.env.AGENTS, app_id);
|
|
1595
|
+
return c.json({
|
|
1596
|
+
success: true,
|
|
1597
|
+
agents: agents.map(agent => ({
|
|
1598
|
+
agent_id: agent.agent_id,
|
|
1599
|
+
app_id: agent.app_id,
|
|
1600
|
+
name: agent.name,
|
|
1601
|
+
operator: agent.operator,
|
|
1602
|
+
version: agent.version,
|
|
1603
|
+
created_at: new Date(agent.created_at).toISOString(),
|
|
1604
|
+
})),
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
catch (error) {
|
|
1608
|
+
return c.json({
|
|
1609
|
+
success: false,
|
|
1610
|
+
error: 'INTERNAL_ERROR',
|
|
1611
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
1612
|
+
}, 500);
|
|
1613
|
+
}
|
|
1614
|
+
});
|
|
1366
1615
|
// ============ DASHBOARD AUTH API ENDPOINTS ============
|
|
1367
1616
|
// Challenge-based dashboard login (agent direct)
|
|
1368
1617
|
app.post('/v1/auth/dashboard', handleDashboardAuthChallenge);
|