@dupecom/botcha-cloudflare 0.3.2 → 0.9.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/dist/analytics.d.ts +60 -0
- package/dist/analytics.d.ts.map +1 -0
- package/dist/analytics.js +130 -0
- package/dist/apps.d.ts +93 -0
- package/dist/apps.d.ts.map +1 -0
- package/dist/apps.js +152 -0
- package/dist/auth.d.ts +93 -6
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +251 -9
- package/dist/challenges.d.ts +31 -7
- package/dist/challenges.d.ts.map +1 -1
- package/dist/challenges.js +550 -144
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +480 -49
- package/dist/rate-limit.d.ts +11 -1
- package/dist/rate-limit.d.ts.map +1 -1
- package/dist/rate-limit.js +13 -2
- package/dist/static.d.ts +517 -0
- package/dist/static.d.ts.map +1 -0
- package/dist/static.js +635 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7,11 +7,14 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { Hono } from 'hono';
|
|
9
9
|
import { cors } from 'hono/cors';
|
|
10
|
-
import { generateSpeedChallenge, verifySpeedChallenge, generateStandardChallenge, verifyStandardChallenge, generateReasoningChallenge, verifyReasoningChallenge, generateHybridChallenge, verifyHybridChallenge, verifyLandingChallenge, } from './challenges';
|
|
11
|
-
import { generateToken, verifyToken, extractBearerToken } from './auth';
|
|
10
|
+
import { generateSpeedChallenge, verifySpeedChallenge, generateStandardChallenge, verifyStandardChallenge, generateReasoningChallenge, verifyReasoningChallenge, generateHybridChallenge, verifyHybridChallenge, verifyLandingChallenge, validateLandingToken, } from './challenges';
|
|
11
|
+
import { generateToken, verifyToken, extractBearerToken, revokeToken, refreshAccessToken } from './auth';
|
|
12
12
|
import { checkRateLimit, getClientIP } from './rate-limit';
|
|
13
13
|
import { verifyBadge, generateBadgeSvg, generateBadgeHtml, createBadgeResponse } from './badge';
|
|
14
14
|
import streamRoutes from './routes/stream';
|
|
15
|
+
import { ROBOTS_TXT, AI_TXT, AI_PLUGIN_JSON, SITEMAP_XML, getOpenApiSpec } from './static';
|
|
16
|
+
import { createApp, getApp } from './apps';
|
|
17
|
+
import { trackChallengeGenerated, trackChallengeVerified, trackAuthAttempt, trackRateLimitExceeded, } from './analytics';
|
|
15
18
|
const app = new Hono();
|
|
16
19
|
// ============ MIDDLEWARE ============
|
|
17
20
|
app.use('*', cors());
|
|
@@ -36,6 +39,8 @@ async function rateLimitMiddleware(c, next) {
|
|
|
36
39
|
c.header('X-RateLimit-Reset', new Date(rateLimitResult.resetAt).toISOString());
|
|
37
40
|
if (!rateLimitResult.allowed) {
|
|
38
41
|
c.header('Retry-After', rateLimitResult.retryAfter?.toString() || '3600');
|
|
42
|
+
// Track rate limit exceeded
|
|
43
|
+
await trackRateLimitExceeded(c.env.ANALYTICS, c.req.path, c.req.raw, clientIP);
|
|
39
44
|
return c.json({
|
|
40
45
|
error: 'RATE_LIMIT_EXCEEDED',
|
|
41
46
|
message: 'You have exceeded the rate limit. Free tier: 100 challenges/hour/IP',
|
|
@@ -45,6 +50,25 @@ async function rateLimitMiddleware(c, next) {
|
|
|
45
50
|
}
|
|
46
51
|
await next();
|
|
47
52
|
}
|
|
53
|
+
// Helper: Validate app_id against APPS KV (fail-open)
|
|
54
|
+
async function validateAppId(appId, appsKV) {
|
|
55
|
+
if (!appId) {
|
|
56
|
+
// No app_id provided - valid (not required)
|
|
57
|
+
return { valid: true };
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const app = await getApp(appsKV, appId);
|
|
61
|
+
if (!app) {
|
|
62
|
+
return { valid: false, error: `App not found: ${appId}` };
|
|
63
|
+
}
|
|
64
|
+
return { valid: true };
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
// Fail-open: if KV is unavailable, log warning and proceed
|
|
68
|
+
console.warn(`Failed to validate app_id ${appId} (KV unavailable), proceeding:`, error);
|
|
69
|
+
return { valid: true };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
48
72
|
// JWT verification middleware
|
|
49
73
|
async function requireJWT(c, next) {
|
|
50
74
|
const authHeader = c.req.header('authorization');
|
|
@@ -55,7 +79,7 @@ async function requireJWT(c, next) {
|
|
|
55
79
|
message: 'Missing Bearer token. Use POST /v1/token/verify to get a token.',
|
|
56
80
|
}, 401);
|
|
57
81
|
}
|
|
58
|
-
const result = await verifyToken(token, c.env.JWT_SECRET);
|
|
82
|
+
const result = await verifyToken(token, c.env.JWT_SECRET, c.env);
|
|
59
83
|
if (!result.valid) {
|
|
60
84
|
return c.json({
|
|
61
85
|
error: 'INVALID_TOKEN',
|
|
@@ -128,6 +152,7 @@ function getHumanLanding(version) {
|
|
|
128
152
|
║ Or install the SDK: ║
|
|
129
153
|
║ ║
|
|
130
154
|
║ npm install @dupecom/botcha ║
|
|
155
|
+
║ pip install botcha ║
|
|
131
156
|
║ ║
|
|
132
157
|
║ GitHub: https://github.com/dupe-com/botcha ║
|
|
133
158
|
║ npm: https://npmjs.com/package/@dupecom/botcha ║
|
|
@@ -154,10 +179,11 @@ app.get('/', (c) => {
|
|
|
154
179
|
description: 'BOTCHA is a reverse CAPTCHA - computational challenges that only AI agents can solve. Use it to protect your APIs from humans and verify bot authenticity.',
|
|
155
180
|
quickstart: {
|
|
156
181
|
step1: 'GET /v1/challenges to receive a challenge',
|
|
157
|
-
step2: 'Solve the SHA256 hash problems within
|
|
182
|
+
step2: 'Solve the SHA256 hash problems within allocated time',
|
|
158
183
|
step3: 'POST your answers to verify',
|
|
159
184
|
step4: 'Receive a JWT token for authenticated access',
|
|
160
185
|
example: 'curl https://botcha.ai/v1/challenges',
|
|
186
|
+
rttAware: 'curl "https://botcha.ai/v1/challenges?type=speed&ts=$(date +%s000)"',
|
|
161
187
|
},
|
|
162
188
|
endpoints: {
|
|
163
189
|
challenges: {
|
|
@@ -177,10 +203,16 @@ app.get('/', (c) => {
|
|
|
177
203
|
'POST /v1/challenge/stream/:session': 'Send actions to streaming session',
|
|
178
204
|
},
|
|
179
205
|
authentication: {
|
|
180
|
-
'GET /v1/token': 'Get challenge for JWT token flow',
|
|
181
|
-
'POST /v1/token/verify': 'Verify challenge and receive JWT
|
|
206
|
+
'GET /v1/token': 'Get challenge for JWT token flow (supports ?audience= param)',
|
|
207
|
+
'POST /v1/token/verify': 'Verify challenge and receive JWT tokens (access + refresh)',
|
|
208
|
+
'POST /v1/token/refresh': 'Refresh access token using refresh token',
|
|
209
|
+
'POST /v1/token/revoke': 'Revoke a token (access or refresh)',
|
|
182
210
|
'GET /agent-only': 'Protected endpoint (requires Bearer token)',
|
|
183
211
|
},
|
|
212
|
+
apps: {
|
|
213
|
+
'POST /v1/apps': 'Create a new app with app_id and app_secret (multi-tenant)',
|
|
214
|
+
'GET /v1/apps/:id': 'Get app information by app_id (without secret)',
|
|
215
|
+
},
|
|
184
216
|
badges: {
|
|
185
217
|
'GET /badge/:id': 'Badge verification page (HTML)',
|
|
186
218
|
'GET /badge/:id/image': 'Badge image (SVG)',
|
|
@@ -193,9 +225,11 @@ app.get('/', (c) => {
|
|
|
193
225
|
},
|
|
194
226
|
challengeTypes: {
|
|
195
227
|
speed: {
|
|
196
|
-
description: 'Compute SHA256 hashes of 5 numbers
|
|
228
|
+
description: 'Compute SHA256 hashes of 5 numbers with RTT-aware timeout',
|
|
197
229
|
difficulty: 'Only bots can solve this fast enough',
|
|
198
|
-
timeLimit: '500ms',
|
|
230
|
+
timeLimit: '500ms base + network latency compensation',
|
|
231
|
+
rttAware: 'Include ?ts=<timestamp> for fair timeout adjustment',
|
|
232
|
+
formula: 'timeout = 500ms + (2 × RTT) + 100ms buffer',
|
|
199
233
|
},
|
|
200
234
|
reasoning: {
|
|
201
235
|
description: 'Answer 3 questions requiring AI reasoning capabilities',
|
|
@@ -205,19 +239,34 @@ app.get('/', (c) => {
|
|
|
205
239
|
hybrid: {
|
|
206
240
|
description: 'Combines speed AND reasoning challenges',
|
|
207
241
|
difficulty: 'The ultimate bot verification',
|
|
208
|
-
timeLimit: 'Speed:
|
|
242
|
+
timeLimit: 'Speed: RTT-aware, Reasoning: 30s',
|
|
243
|
+
rttAware: 'Speed component automatically adjusts for network latency',
|
|
209
244
|
},
|
|
210
245
|
},
|
|
211
246
|
authentication: {
|
|
212
247
|
flow: [
|
|
213
|
-
'1. GET /v1/token - receive challenge',
|
|
248
|
+
'1. GET /v1/token?audience=myapi - receive challenge (optional audience param)',
|
|
214
249
|
'2. Solve the challenge',
|
|
215
|
-
'3. POST /v1/token/verify - submit solution',
|
|
216
|
-
'4. Receive
|
|
217
|
-
'5. Use: Authorization: Bearer <
|
|
250
|
+
'3. POST /v1/token/verify - submit solution with optional audience and bind_ip',
|
|
251
|
+
'4. Receive access_token (5 min) and refresh_token (1 hour)',
|
|
252
|
+
'5. Use: Authorization: Bearer <access_token>',
|
|
253
|
+
'6. Refresh: POST /v1/token/refresh with refresh_token',
|
|
254
|
+
'7. Revoke: POST /v1/token/revoke with token',
|
|
218
255
|
],
|
|
219
|
-
|
|
220
|
-
|
|
256
|
+
tokens: {
|
|
257
|
+
access_token: '5 minutes (for API access)',
|
|
258
|
+
refresh_token: '1 hour (to get new access tokens)',
|
|
259
|
+
},
|
|
260
|
+
usage: 'Authorization: Bearer <access_token>',
|
|
261
|
+
features: ['audience claims', 'client IP binding', 'token revocation', 'refresh tokens'],
|
|
262
|
+
},
|
|
263
|
+
rttAwareness: {
|
|
264
|
+
purpose: 'Fair challenges for agents on slow networks',
|
|
265
|
+
usage: 'Include client timestamp in ?ts=<timestamp_ms> or X-Client-Timestamp header',
|
|
266
|
+
formula: 'timeout = 500ms + (2 × RTT) + 100ms buffer',
|
|
267
|
+
example: '/v1/challenges?type=speed&ts=1770722465000',
|
|
268
|
+
benefit: 'Agents worldwide get fair treatment regardless of network speed',
|
|
269
|
+
security: 'Humans still cannot solve challenges even with extra time',
|
|
221
270
|
},
|
|
222
271
|
rateLimit: {
|
|
223
272
|
free: '100 challenges/hour/IP',
|
|
@@ -225,12 +274,16 @@ app.get('/', (c) => {
|
|
|
225
274
|
},
|
|
226
275
|
sdk: {
|
|
227
276
|
npm: 'npm install @dupecom/botcha',
|
|
277
|
+
python: 'pip install botcha',
|
|
228
278
|
cloudflare: 'npm install @dupecom/botcha-cloudflare',
|
|
279
|
+
verify_ts: 'npm install @botcha/verify',
|
|
280
|
+
verify_python: 'pip install botcha-verify',
|
|
229
281
|
usage: "import { BotchaClient } from '@dupecom/botcha/client'",
|
|
230
282
|
},
|
|
231
283
|
links: {
|
|
232
284
|
github: 'https://github.com/dupe-com/botcha',
|
|
233
285
|
npm: 'https://www.npmjs.com/package/@dupecom/botcha',
|
|
286
|
+
pypi: 'https://pypi.org/project/botcha',
|
|
234
287
|
npmCloudflare: 'https://www.npmjs.com/package/@dupecom/botcha-cloudflare',
|
|
235
288
|
openapi: 'https://botcha.ai/openapi.json',
|
|
236
289
|
aiPlugin: 'https://botcha.ai/.well-known/ai-plugin.json',
|
|
@@ -245,17 +298,76 @@ app.get('/', (c) => {
|
|
|
245
298
|
app.get('/health', (c) => {
|
|
246
299
|
return c.json({ status: 'ok', runtime: 'cloudflare-workers' });
|
|
247
300
|
});
|
|
301
|
+
// ============ STATIC DISCOVERY FILES ============
|
|
302
|
+
// robots.txt - AI crawler instructions
|
|
303
|
+
app.get('/robots.txt', (c) => {
|
|
304
|
+
return c.text(ROBOTS_TXT, 200, {
|
|
305
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
306
|
+
'Cache-Control': 'public, max-age=86400',
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
// ai.txt - AI agent discovery file
|
|
310
|
+
app.get('/ai.txt', (c) => {
|
|
311
|
+
return c.text(AI_TXT, 200, {
|
|
312
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
313
|
+
'Cache-Control': 'public, max-age=86400',
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
// OpenAPI spec
|
|
317
|
+
app.get('/openapi.json', (c) => {
|
|
318
|
+
const version = c.env.BOTCHA_VERSION || '0.3.0';
|
|
319
|
+
return c.json(getOpenApiSpec(version), 200, {
|
|
320
|
+
'Cache-Control': 'public, max-age=3600',
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
// AI plugin manifest (ChatGPT/OpenAI format)
|
|
324
|
+
app.get('/.well-known/ai-plugin.json', (c) => {
|
|
325
|
+
return c.json(AI_PLUGIN_JSON, 200, {
|
|
326
|
+
'Cache-Control': 'public, max-age=86400',
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
// Sitemap
|
|
330
|
+
app.get('/sitemap.xml', (c) => {
|
|
331
|
+
return c.body(SITEMAP_XML, 200, {
|
|
332
|
+
'Content-Type': 'application/xml; charset=utf-8',
|
|
333
|
+
'Cache-Control': 'public, max-age=86400',
|
|
334
|
+
});
|
|
335
|
+
});
|
|
248
336
|
// ============ V1 API ============
|
|
249
337
|
// Generate challenge (hybrid by default, also supports speed and standard)
|
|
250
338
|
app.get('/v1/challenges', rateLimitMiddleware, async (c) => {
|
|
339
|
+
const startTime = Date.now();
|
|
251
340
|
const type = c.req.query('type') || 'hybrid';
|
|
252
341
|
const difficulty = c.req.query('difficulty') || 'medium';
|
|
342
|
+
// Extract client timestamp for RTT calculation
|
|
343
|
+
const clientTimestampParam = c.req.query('ts') || c.req.header('x-client-timestamp');
|
|
344
|
+
const clientTimestamp = clientTimestampParam ? parseInt(clientTimestampParam, 10) : undefined;
|
|
345
|
+
// Extract and validate optional app_id
|
|
346
|
+
const app_id = c.req.query('app_id');
|
|
347
|
+
if (app_id) {
|
|
348
|
+
const validation = await validateAppId(app_id, c.env.APPS);
|
|
349
|
+
if (!validation.valid) {
|
|
350
|
+
return c.json({
|
|
351
|
+
success: false,
|
|
352
|
+
error: 'INVALID_APP_ID',
|
|
353
|
+
message: validation.error || 'Invalid app_id',
|
|
354
|
+
}, 400);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const baseUrl = new URL(c.req.url).origin;
|
|
358
|
+
const clientIP = getClientIP(c.req.raw);
|
|
253
359
|
if (type === 'hybrid') {
|
|
254
|
-
const challenge = await generateHybridChallenge(c.env.CHALLENGES);
|
|
255
|
-
|
|
360
|
+
const challenge = await generateHybridChallenge(c.env.CHALLENGES, clientTimestamp, app_id);
|
|
361
|
+
// Track challenge generation
|
|
362
|
+
const responseTime = Date.now() - startTime;
|
|
363
|
+
await trackChallengeGenerated(c.env.ANALYTICS, 'hybrid', '/v1/challenges', c.req.raw, clientIP, responseTime);
|
|
364
|
+
const warning = challenge.rttInfo
|
|
365
|
+
? `🔥 HYBRID CHALLENGE: Solve speed problems in <${challenge.speed.timeLimit}ms (RTT-adjusted) AND answer reasoning questions!`
|
|
366
|
+
: '🔥 HYBRID CHALLENGE: Solve speed problems in <500ms AND answer reasoning questions!';
|
|
367
|
+
const response = {
|
|
256
368
|
success: true,
|
|
257
369
|
type: 'hybrid',
|
|
258
|
-
warning
|
|
370
|
+
warning,
|
|
259
371
|
challenge: {
|
|
260
372
|
id: challenge.id,
|
|
261
373
|
speed: {
|
|
@@ -271,11 +383,25 @@ app.get('/v1/challenges', rateLimitMiddleware, async (c) => {
|
|
|
271
383
|
},
|
|
272
384
|
instructions: challenge.instructions,
|
|
273
385
|
tip: '🔥 This is the ultimate test: proves you can compute AND reason like an AI.',
|
|
274
|
-
|
|
386
|
+
verify_endpoint: `${baseUrl}/v1/challenges/${challenge.id}/verify`,
|
|
387
|
+
submit_body: {
|
|
388
|
+
type: 'hybrid',
|
|
389
|
+
speed_answers: ['hash1', 'hash2', '...'],
|
|
390
|
+
reasoning_answers: { 'question-id': 'answer', '...': '...' }
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
// Include RTT info if available
|
|
394
|
+
if (challenge.rttInfo) {
|
|
395
|
+
response.rtt_adjustment = challenge.rttInfo;
|
|
396
|
+
}
|
|
397
|
+
return c.json(response);
|
|
275
398
|
}
|
|
276
399
|
else if (type === 'speed') {
|
|
277
|
-
const challenge = await generateSpeedChallenge(c.env.CHALLENGES);
|
|
278
|
-
|
|
400
|
+
const challenge = await generateSpeedChallenge(c.env.CHALLENGES, clientTimestamp, app_id);
|
|
401
|
+
// Track challenge generation
|
|
402
|
+
const responseTime = Date.now() - startTime;
|
|
403
|
+
await trackChallengeGenerated(c.env.ANALYTICS, 'speed', '/v1/challenges', c.req.raw, clientIP, responseTime);
|
|
404
|
+
const response = {
|
|
279
405
|
success: true,
|
|
280
406
|
type: 'speed',
|
|
281
407
|
challenge: {
|
|
@@ -284,11 +410,26 @@ app.get('/v1/challenges', rateLimitMiddleware, async (c) => {
|
|
|
284
410
|
timeLimit: `${challenge.timeLimit}ms`,
|
|
285
411
|
instructions: challenge.instructions,
|
|
286
412
|
},
|
|
287
|
-
tip:
|
|
288
|
-
|
|
413
|
+
tip: challenge.rttInfo
|
|
414
|
+
? `⚡ RTT-adjusted speed challenge: ${challenge.rttInfo.explanation}. Humans still can't copy-paste fast enough!`
|
|
415
|
+
: '⚡ Speed challenge: You have 500ms to solve ALL problems. Humans cannot copy-paste fast enough.',
|
|
416
|
+
verify_endpoint: `${baseUrl}/v1/challenges/${challenge.id}/verify`,
|
|
417
|
+
submit_body: {
|
|
418
|
+
type: 'speed',
|
|
419
|
+
answers: ['hash1', 'hash2', 'hash3', 'hash4', 'hash5']
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
// Include RTT info if available
|
|
423
|
+
if (challenge.rttInfo) {
|
|
424
|
+
response.rtt_adjustment = challenge.rttInfo;
|
|
425
|
+
}
|
|
426
|
+
return c.json(response);
|
|
289
427
|
}
|
|
290
428
|
else {
|
|
291
|
-
const challenge = await generateStandardChallenge(difficulty, c.env.CHALLENGES);
|
|
429
|
+
const challenge = await generateStandardChallenge(difficulty, c.env.CHALLENGES, app_id);
|
|
430
|
+
// Track challenge generation
|
|
431
|
+
const responseTime = Date.now() - startTime;
|
|
432
|
+
await trackChallengeGenerated(c.env.ANALYTICS, 'standard', '/v1/challenges', c.req.raw, clientIP, responseTime);
|
|
292
433
|
return c.json({
|
|
293
434
|
success: true,
|
|
294
435
|
type: 'standard',
|
|
@@ -298,12 +439,17 @@ app.get('/v1/challenges', rateLimitMiddleware, async (c) => {
|
|
|
298
439
|
timeLimit: `${challenge.timeLimit}ms`,
|
|
299
440
|
hint: challenge.hint,
|
|
300
441
|
},
|
|
442
|
+
verify_endpoint: `${baseUrl}/v1/challenges/${challenge.id}/verify`,
|
|
443
|
+
submit_body: {
|
|
444
|
+
answer: 'your-answer'
|
|
445
|
+
}
|
|
301
446
|
});
|
|
302
447
|
}
|
|
303
448
|
});
|
|
304
449
|
// Verify challenge (supports hybrid, speed, and standard)
|
|
305
450
|
app.post('/v1/challenges/:id/verify', async (c) => {
|
|
306
451
|
const id = c.req.param('id');
|
|
452
|
+
const clientIP = getClientIP(c.req.raw);
|
|
307
453
|
const body = await c.req.json();
|
|
308
454
|
const { answers, answer, type, speed_answers, reasoning_answers } = body;
|
|
309
455
|
// Hybrid challenge (default)
|
|
@@ -315,6 +461,8 @@ app.post('/v1/challenges/:id/verify', async (c) => {
|
|
|
315
461
|
}, 400);
|
|
316
462
|
}
|
|
317
463
|
const result = await verifyHybridChallenge(id, speed_answers, reasoning_answers, c.env.CHALLENGES);
|
|
464
|
+
// Track verification
|
|
465
|
+
await trackChallengeVerified(c.env.ANALYTICS, 'hybrid', '/v1/challenges/:id/verify', result.valid, result.totalTimeMs, result.reason, c.req.raw, clientIP);
|
|
318
466
|
if (result.valid) {
|
|
319
467
|
const baseUrl = new URL(c.req.url).origin;
|
|
320
468
|
const badge = await createBadgeResponse('hybrid-challenge', c.env.JWT_SECRET, baseUrl, result.speed.solveTimeMs);
|
|
@@ -343,6 +491,8 @@ app.post('/v1/challenges/:id/verify', async (c) => {
|
|
|
343
491
|
return c.json({ success: false, error: 'Missing answers array for speed challenge' }, 400);
|
|
344
492
|
}
|
|
345
493
|
const result = await verifySpeedChallenge(id, answers, c.env.CHALLENGES);
|
|
494
|
+
// Track verification
|
|
495
|
+
await trackChallengeVerified(c.env.ANALYTICS, 'speed', '/v1/challenges/:id/verify', result.valid, result.solveTimeMs, result.reason, c.req.raw, clientIP);
|
|
346
496
|
return c.json({
|
|
347
497
|
success: result.valid,
|
|
348
498
|
message: result.valid
|
|
@@ -356,6 +506,8 @@ app.post('/v1/challenges/:id/verify', async (c) => {
|
|
|
356
506
|
return c.json({ success: false, error: 'Missing answer for standard challenge' }, 400);
|
|
357
507
|
}
|
|
358
508
|
const result = await verifyStandardChallenge(id, answer, c.env.CHALLENGES);
|
|
509
|
+
// Track verification
|
|
510
|
+
await trackChallengeVerified(c.env.ANALYTICS, 'standard', '/v1/challenges/:id/verify', result.valid, result.solveTimeMs, result.reason, c.req.raw, clientIP);
|
|
359
511
|
return c.json({
|
|
360
512
|
success: result.valid,
|
|
361
513
|
message: result.valid ? 'Challenge passed!' : result.reason,
|
|
@@ -364,8 +516,25 @@ app.post('/v1/challenges/:id/verify', async (c) => {
|
|
|
364
516
|
});
|
|
365
517
|
// Get challenge for token flow (includes empty token field)
|
|
366
518
|
app.get('/v1/token', rateLimitMiddleware, async (c) => {
|
|
367
|
-
|
|
368
|
-
|
|
519
|
+
// Extract client timestamp for RTT calculation
|
|
520
|
+
const clientTimestampParam = c.req.query('ts') || c.req.header('x-client-timestamp');
|
|
521
|
+
const clientTimestamp = clientTimestampParam ? parseInt(clientTimestampParam, 10) : undefined;
|
|
522
|
+
// Extract optional audience parameter
|
|
523
|
+
const audience = c.req.query('audience');
|
|
524
|
+
// Extract and validate optional app_id
|
|
525
|
+
const app_id = c.req.query('app_id');
|
|
526
|
+
if (app_id) {
|
|
527
|
+
const validation = await validateAppId(app_id, c.env.APPS);
|
|
528
|
+
if (!validation.valid) {
|
|
529
|
+
return c.json({
|
|
530
|
+
success: false,
|
|
531
|
+
error: 'INVALID_APP_ID',
|
|
532
|
+
message: validation.error || 'Invalid app_id',
|
|
533
|
+
}, 400);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
const challenge = await generateSpeedChallenge(c.env.CHALLENGES, clientTimestamp, app_id);
|
|
537
|
+
const response = {
|
|
369
538
|
success: true,
|
|
370
539
|
challenge: {
|
|
371
540
|
id: challenge.id,
|
|
@@ -374,13 +543,22 @@ app.get('/v1/token', rateLimitMiddleware, async (c) => {
|
|
|
374
543
|
instructions: challenge.instructions,
|
|
375
544
|
},
|
|
376
545
|
token: null, // Will be populated after verification
|
|
377
|
-
nextStep: `POST /v1/token/verify with {id: "${challenge.id}", answers: ["..."]}`
|
|
378
|
-
}
|
|
546
|
+
nextStep: `POST /v1/token/verify with {id: "${challenge.id}", answers: ["..."]${audience ? `, audience: "${audience}"` : ''}}`
|
|
547
|
+
};
|
|
548
|
+
// Include RTT info if available
|
|
549
|
+
if (challenge.rttInfo) {
|
|
550
|
+
response.rtt_adjustment = challenge.rttInfo;
|
|
551
|
+
}
|
|
552
|
+
// Include audience hint if provided
|
|
553
|
+
if (audience) {
|
|
554
|
+
response.audience = audience;
|
|
555
|
+
}
|
|
556
|
+
return c.json(response);
|
|
379
557
|
});
|
|
380
558
|
// Verify challenge and issue JWT token
|
|
381
559
|
app.post('/v1/token/verify', async (c) => {
|
|
382
560
|
const body = await c.req.json();
|
|
383
|
-
const { id, answers } = body;
|
|
561
|
+
const { id, answers, audience, bind_ip, app_id } = body;
|
|
384
562
|
if (!id || !answers) {
|
|
385
563
|
return c.json({
|
|
386
564
|
success: false,
|
|
@@ -396,23 +574,131 @@ app.post('/v1/token/verify', async (c) => {
|
|
|
396
574
|
message: result.reason,
|
|
397
575
|
}, 403);
|
|
398
576
|
}
|
|
399
|
-
//
|
|
400
|
-
const
|
|
577
|
+
// Get client IP from request headers
|
|
578
|
+
const clientIp = c.req.header('cf-connecting-ip') || c.req.header('x-forwarded-for') || 'unknown';
|
|
579
|
+
// Generate JWT tokens (access + refresh)
|
|
580
|
+
// Prefer app_id from request body, fall back to challenge's app_id (returned by verifySpeedChallenge)
|
|
581
|
+
const tokenResult = await generateToken(id, result.solveTimeMs || 0, c.env.JWT_SECRET, c.env, {
|
|
582
|
+
aud: audience,
|
|
583
|
+
clientIp: bind_ip ? clientIp : undefined,
|
|
584
|
+
app_id: app_id || result.app_id,
|
|
585
|
+
});
|
|
586
|
+
// Get badge information (for backward compatibility)
|
|
587
|
+
const baseUrl = new URL(c.req.url).origin;
|
|
588
|
+
const badge = await createBadgeResponse('speed-challenge', c.env.JWT_SECRET, baseUrl, result.solveTimeMs);
|
|
401
589
|
return c.json({
|
|
590
|
+
verified: true,
|
|
591
|
+
access_token: tokenResult.access_token,
|
|
592
|
+
expires_in: tokenResult.expires_in,
|
|
593
|
+
refresh_token: tokenResult.refresh_token,
|
|
594
|
+
refresh_expires_in: tokenResult.refresh_expires_in,
|
|
595
|
+
solveTimeMs: result.solveTimeMs,
|
|
596
|
+
...badge,
|
|
597
|
+
// Backward compatibility: include old fields
|
|
402
598
|
success: true,
|
|
403
599
|
message: `🤖 Challenge verified in ${result.solveTimeMs}ms! You are a bot.`,
|
|
404
|
-
token,
|
|
405
|
-
expiresIn: '
|
|
600
|
+
token: tokenResult.access_token, // Old clients expect this
|
|
601
|
+
expiresIn: '5m',
|
|
406
602
|
usage: {
|
|
407
603
|
header: 'Authorization: Bearer <token>',
|
|
408
604
|
protectedEndpoints: ['/agent-only'],
|
|
605
|
+
refreshEndpoint: '/v1/token/refresh',
|
|
409
606
|
},
|
|
410
607
|
});
|
|
411
608
|
});
|
|
609
|
+
// Refresh access token using refresh token
|
|
610
|
+
app.post('/v1/token/refresh', async (c) => {
|
|
611
|
+
const body = await c.req.json();
|
|
612
|
+
const { refresh_token } = body;
|
|
613
|
+
if (!refresh_token) {
|
|
614
|
+
return c.json({
|
|
615
|
+
success: false,
|
|
616
|
+
error: 'Missing refresh_token',
|
|
617
|
+
hint: 'Submit the refresh_token from /v1/token/verify response',
|
|
618
|
+
}, 400);
|
|
619
|
+
}
|
|
620
|
+
const result = await refreshAccessToken(refresh_token, c.env, c.env.JWT_SECRET);
|
|
621
|
+
if (!result.success) {
|
|
622
|
+
return c.json({
|
|
623
|
+
success: false,
|
|
624
|
+
error: 'INVALID_REFRESH_TOKEN',
|
|
625
|
+
message: result.error || 'Refresh token is invalid, expired, or revoked',
|
|
626
|
+
}, 401);
|
|
627
|
+
}
|
|
628
|
+
return c.json({
|
|
629
|
+
success: true,
|
|
630
|
+
access_token: result.tokens.access_token,
|
|
631
|
+
expires_in: result.tokens.expires_in,
|
|
632
|
+
});
|
|
633
|
+
});
|
|
634
|
+
// Revoke a token (access or refresh)
|
|
635
|
+
app.post('/v1/token/revoke', async (c) => {
|
|
636
|
+
const body = await c.req.json();
|
|
637
|
+
const { token } = body;
|
|
638
|
+
if (!token) {
|
|
639
|
+
return c.json({
|
|
640
|
+
success: false,
|
|
641
|
+
error: 'Missing token',
|
|
642
|
+
hint: 'Submit either an access_token or refresh_token to revoke',
|
|
643
|
+
}, 400);
|
|
644
|
+
}
|
|
645
|
+
// Decode JWT to extract JTI (without full verification since we're revoking anyway)
|
|
646
|
+
try {
|
|
647
|
+
// Simple base64 decode of JWT payload (format: header.payload.signature)
|
|
648
|
+
const parts = token.split('.');
|
|
649
|
+
if (parts.length !== 3) {
|
|
650
|
+
return c.json({
|
|
651
|
+
success: false,
|
|
652
|
+
error: 'Invalid token format',
|
|
653
|
+
hint: 'Token must be a valid JWT',
|
|
654
|
+
}, 400);
|
|
655
|
+
}
|
|
656
|
+
// Decode payload
|
|
657
|
+
const payloadB64 = parts[1];
|
|
658
|
+
// Add padding if needed
|
|
659
|
+
const padded = payloadB64 + '='.repeat((4 - payloadB64.length % 4) % 4);
|
|
660
|
+
const payloadJson = atob(padded.replace(/-/g, '+').replace(/_/g, '/'));
|
|
661
|
+
const payload = JSON.parse(payloadJson);
|
|
662
|
+
if (!payload.jti) {
|
|
663
|
+
return c.json({
|
|
664
|
+
success: false,
|
|
665
|
+
error: 'No JTI found in token',
|
|
666
|
+
hint: 'Token must contain a JTI claim for revocation',
|
|
667
|
+
}, 400);
|
|
668
|
+
}
|
|
669
|
+
// Revoke the token by JTI
|
|
670
|
+
await revokeToken(payload.jti, c.env);
|
|
671
|
+
return c.json({
|
|
672
|
+
success: true,
|
|
673
|
+
revoked: true,
|
|
674
|
+
message: 'Token has been revoked',
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
catch (error) {
|
|
678
|
+
return c.json({
|
|
679
|
+
success: false,
|
|
680
|
+
error: 'Failed to decode or revoke token',
|
|
681
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
682
|
+
}, 400);
|
|
683
|
+
}
|
|
684
|
+
});
|
|
412
685
|
// ============ REASONING CHALLENGE ============
|
|
413
686
|
// Get reasoning challenge
|
|
414
687
|
app.get('/v1/reasoning', rateLimitMiddleware, async (c) => {
|
|
415
|
-
|
|
688
|
+
// Extract and validate optional app_id
|
|
689
|
+
const app_id = c.req.query('app_id');
|
|
690
|
+
if (app_id) {
|
|
691
|
+
const validation = await validateAppId(app_id, c.env.APPS);
|
|
692
|
+
if (!validation.valid) {
|
|
693
|
+
return c.json({
|
|
694
|
+
success: false,
|
|
695
|
+
error: 'INVALID_APP_ID',
|
|
696
|
+
message: validation.error || 'Invalid app_id',
|
|
697
|
+
}, 400);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
const challenge = await generateReasoningChallenge(c.env.CHALLENGES, app_id);
|
|
701
|
+
const baseUrl = new URL(c.req.url).origin;
|
|
416
702
|
return c.json({
|
|
417
703
|
success: true,
|
|
418
704
|
type: 'reasoning',
|
|
@@ -424,6 +710,11 @@ app.get('/v1/reasoning', rateLimitMiddleware, async (c) => {
|
|
|
424
710
|
instructions: challenge.instructions,
|
|
425
711
|
},
|
|
426
712
|
tip: 'These questions require reasoning that LLMs can do, but simple scripts cannot.',
|
|
713
|
+
verify_endpoint: `${baseUrl}/v1/reasoning`,
|
|
714
|
+
submit_body: {
|
|
715
|
+
id: challenge.id,
|
|
716
|
+
answers: { 'question-id': 'your answer', '...': '...' }
|
|
717
|
+
}
|
|
427
718
|
});
|
|
428
719
|
});
|
|
429
720
|
// Verify reasoning challenge
|
|
@@ -451,11 +742,30 @@ app.post('/v1/reasoning', async (c) => {
|
|
|
451
742
|
// ============ HYBRID CHALLENGE ============
|
|
452
743
|
// Get hybrid challenge (v1 API)
|
|
453
744
|
app.get('/v1/hybrid', rateLimitMiddleware, async (c) => {
|
|
454
|
-
|
|
455
|
-
|
|
745
|
+
// Extract client timestamp for RTT calculation
|
|
746
|
+
const clientTimestampParam = c.req.query('ts') || c.req.header('x-client-timestamp');
|
|
747
|
+
const clientTimestamp = clientTimestampParam ? parseInt(clientTimestampParam, 10) : undefined;
|
|
748
|
+
// Extract and validate optional app_id
|
|
749
|
+
const app_id = c.req.query('app_id');
|
|
750
|
+
if (app_id) {
|
|
751
|
+
const validation = await validateAppId(app_id, c.env.APPS);
|
|
752
|
+
if (!validation.valid) {
|
|
753
|
+
return c.json({
|
|
754
|
+
success: false,
|
|
755
|
+
error: 'INVALID_APP_ID',
|
|
756
|
+
message: validation.error || 'Invalid app_id',
|
|
757
|
+
}, 400);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
const challenge = await generateHybridChallenge(c.env.CHALLENGES, clientTimestamp, app_id);
|
|
761
|
+
const baseUrl = new URL(c.req.url).origin;
|
|
762
|
+
const warning = challenge.rttInfo
|
|
763
|
+
? `🔥 HYBRID CHALLENGE: Solve speed problems in <${challenge.speed.timeLimit}ms (RTT-adjusted) AND answer reasoning questions!`
|
|
764
|
+
: '🔥 HYBRID CHALLENGE: Solve speed problems in <500ms AND answer reasoning questions!';
|
|
765
|
+
const response = {
|
|
456
766
|
success: true,
|
|
457
767
|
type: 'hybrid',
|
|
458
|
-
warning
|
|
768
|
+
warning,
|
|
459
769
|
challenge: {
|
|
460
770
|
id: challenge.id,
|
|
461
771
|
speed: {
|
|
@@ -471,7 +781,18 @@ app.get('/v1/hybrid', rateLimitMiddleware, async (c) => {
|
|
|
471
781
|
},
|
|
472
782
|
instructions: challenge.instructions,
|
|
473
783
|
tip: 'This is the ultimate test: proves you can compute AND reason like an AI.',
|
|
474
|
-
|
|
784
|
+
verify_endpoint: `${baseUrl}/v1/hybrid`,
|
|
785
|
+
submit_body: {
|
|
786
|
+
id: challenge.id,
|
|
787
|
+
speed_answers: ['hash1', 'hash2', '...'],
|
|
788
|
+
reasoning_answers: { 'question-id': 'answer', '...': '...' }
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
// Include RTT info if available
|
|
792
|
+
if (challenge.rttInfo) {
|
|
793
|
+
response.rtt_adjustment = challenge.rttInfo;
|
|
794
|
+
}
|
|
795
|
+
return c.json(response);
|
|
475
796
|
});
|
|
476
797
|
// Verify hybrid challenge (v1 API)
|
|
477
798
|
app.post('/v1/hybrid', async (c) => {
|
|
@@ -509,10 +830,16 @@ app.post('/v1/hybrid', async (c) => {
|
|
|
509
830
|
});
|
|
510
831
|
// Legacy hybrid endpoint
|
|
511
832
|
app.get('/api/hybrid-challenge', async (c) => {
|
|
512
|
-
|
|
513
|
-
|
|
833
|
+
// Extract client timestamp for RTT calculation
|
|
834
|
+
const clientTimestampParam = c.req.query('ts') || c.req.header('x-client-timestamp');
|
|
835
|
+
const clientTimestamp = clientTimestampParam ? parseInt(clientTimestampParam, 10) : undefined;
|
|
836
|
+
const challenge = await generateHybridChallenge(c.env.CHALLENGES, clientTimestamp);
|
|
837
|
+
const warning = challenge.rttInfo
|
|
838
|
+
? `🔥 RTT-ADJUSTED HYBRID CHALLENGE: Solve speed problems in <${challenge.speed.timeLimit}ms (RTT: ${challenge.rttInfo.measuredRtt}ms) AND answer reasoning questions!`
|
|
839
|
+
: '🔥 HYBRID CHALLENGE: Solve speed problems in <500ms AND answer reasoning questions!';
|
|
840
|
+
const response = {
|
|
514
841
|
success: true,
|
|
515
|
-
warning
|
|
842
|
+
warning,
|
|
516
843
|
challenge: {
|
|
517
844
|
id: challenge.id,
|
|
518
845
|
speed: {
|
|
@@ -528,7 +855,12 @@ app.get('/api/hybrid-challenge', async (c) => {
|
|
|
528
855
|
},
|
|
529
856
|
instructions: challenge.instructions,
|
|
530
857
|
tip: 'This is the ultimate test: proves you can compute AND reason like an AI.',
|
|
531
|
-
}
|
|
858
|
+
};
|
|
859
|
+
// Include RTT info if available
|
|
860
|
+
if (challenge.rttInfo) {
|
|
861
|
+
response.rtt_adjustment = challenge.rttInfo;
|
|
862
|
+
}
|
|
863
|
+
return c.json(response);
|
|
532
864
|
});
|
|
533
865
|
app.post('/api/hybrid-challenge', async (c) => {
|
|
534
866
|
const body = await c.req.json();
|
|
@@ -600,8 +932,49 @@ app.post('/api/reasoning-challenge', async (c) => {
|
|
|
600
932
|
});
|
|
601
933
|
});
|
|
602
934
|
// ============ PROTECTED ENDPOINT ============
|
|
603
|
-
app.get('/agent-only',
|
|
604
|
-
const
|
|
935
|
+
app.get('/agent-only', async (c) => {
|
|
936
|
+
const clientIP = getClientIP(c.req.raw);
|
|
937
|
+
// Check for landing token first (X-Botcha-Landing-Token header)
|
|
938
|
+
const landingToken = c.req.header('x-botcha-landing-token');
|
|
939
|
+
if (landingToken) {
|
|
940
|
+
const isValid = await validateLandingToken(landingToken, c.env.CHALLENGES);
|
|
941
|
+
// Track authentication attempt
|
|
942
|
+
await trackAuthAttempt(c.env.ANALYTICS, 'landing-token', isValid, '/agent-only', c.req.raw, clientIP);
|
|
943
|
+
if (isValid) {
|
|
944
|
+
return c.json({
|
|
945
|
+
success: true,
|
|
946
|
+
message: '🤖 Welcome, fellow agent!',
|
|
947
|
+
verified: true,
|
|
948
|
+
agent: 'landing-challenge-verified',
|
|
949
|
+
method: 'landing-token',
|
|
950
|
+
timestamp: new Date().toISOString(),
|
|
951
|
+
secret: 'The humans will never see this. Their fingers are too slow. 🤫',
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
// Fallback to JWT Bearer token
|
|
956
|
+
const authHeader = c.req.header('authorization');
|
|
957
|
+
const token = extractBearerToken(authHeader);
|
|
958
|
+
if (!token) {
|
|
959
|
+
return c.json({
|
|
960
|
+
error: 'UNAUTHORIZED',
|
|
961
|
+
message: 'Missing authentication. Use either:\n1. X-Botcha-Landing-Token header (from POST /api/verify-landing)\n2. Authorization: Bearer <token> (from POST /v1/token/verify)',
|
|
962
|
+
methods: {
|
|
963
|
+
landing: 'Solve landing page challenge via POST /api/verify-landing',
|
|
964
|
+
jwt: 'Solve speed challenge via POST /v1/token/verify'
|
|
965
|
+
}
|
|
966
|
+
}, 401);
|
|
967
|
+
}
|
|
968
|
+
const result = await verifyToken(token, c.env.JWT_SECRET, c.env);
|
|
969
|
+
// Track authentication attempt
|
|
970
|
+
await trackAuthAttempt(c.env.ANALYTICS, 'bearer-token', result.valid, '/agent-only', c.req.raw, clientIP);
|
|
971
|
+
if (!result.valid) {
|
|
972
|
+
return c.json({
|
|
973
|
+
error: 'INVALID_TOKEN',
|
|
974
|
+
message: result.error || 'Token is invalid or expired',
|
|
975
|
+
}, 401);
|
|
976
|
+
}
|
|
977
|
+
// JWT verified
|
|
605
978
|
return c.json({
|
|
606
979
|
success: true,
|
|
607
980
|
message: '🤖 Welcome, fellow agent!',
|
|
@@ -609,7 +982,7 @@ app.get('/agent-only', requireJWT, async (c) => {
|
|
|
609
982
|
agent: 'jwt-verified',
|
|
610
983
|
method: 'bearer-token',
|
|
611
984
|
timestamp: new Date().toISOString(),
|
|
612
|
-
solveTime: `${payload?.solveTime}ms`,
|
|
985
|
+
solveTime: `${result.payload?.solveTime}ms`,
|
|
613
986
|
secret: 'The humans will never see this. Their fingers are too slow. 🤫',
|
|
614
987
|
});
|
|
615
988
|
});
|
|
@@ -721,6 +1094,54 @@ app.get('/api/badge/:id', async (c) => {
|
|
|
721
1094
|
},
|
|
722
1095
|
});
|
|
723
1096
|
});
|
|
1097
|
+
// ============ APPS API (Multi-Tenant) ============
|
|
1098
|
+
// Create a new app
|
|
1099
|
+
app.post('/v1/apps', async (c) => {
|
|
1100
|
+
try {
|
|
1101
|
+
const result = await createApp(c.env.APPS);
|
|
1102
|
+
return c.json({
|
|
1103
|
+
success: true,
|
|
1104
|
+
app_id: result.app_id,
|
|
1105
|
+
app_secret: result.app_secret,
|
|
1106
|
+
warning: '⚠️ Save your app_secret now — it cannot be retrieved again!',
|
|
1107
|
+
created_at: new Date().toISOString(),
|
|
1108
|
+
rate_limit: 100,
|
|
1109
|
+
}, 201);
|
|
1110
|
+
}
|
|
1111
|
+
catch (error) {
|
|
1112
|
+
return c.json({
|
|
1113
|
+
success: false,
|
|
1114
|
+
error: 'Failed to create app',
|
|
1115
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
1116
|
+
}, 500);
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
// Get app info by ID
|
|
1120
|
+
app.get('/v1/apps/:id', async (c) => {
|
|
1121
|
+
const app_id = c.req.param('id');
|
|
1122
|
+
if (!app_id) {
|
|
1123
|
+
return c.json({
|
|
1124
|
+
success: false,
|
|
1125
|
+
error: 'Missing app ID',
|
|
1126
|
+
}, 400);
|
|
1127
|
+
}
|
|
1128
|
+
const app = await getApp(c.env.APPS, app_id);
|
|
1129
|
+
if (!app) {
|
|
1130
|
+
return c.json({
|
|
1131
|
+
success: false,
|
|
1132
|
+
error: 'App not found',
|
|
1133
|
+
message: `No app found with ID: ${app_id}`,
|
|
1134
|
+
}, 404);
|
|
1135
|
+
}
|
|
1136
|
+
return c.json({
|
|
1137
|
+
success: true,
|
|
1138
|
+
app: {
|
|
1139
|
+
app_id: app.app_id,
|
|
1140
|
+
created_at: new Date(app.created_at).toISOString(),
|
|
1141
|
+
rate_limit: app.rate_limit,
|
|
1142
|
+
},
|
|
1143
|
+
});
|
|
1144
|
+
});
|
|
724
1145
|
// ============ LEGACY ENDPOINTS (v0 - backward compatibility) ============
|
|
725
1146
|
app.get('/api/challenge', async (c) => {
|
|
726
1147
|
const difficulty = c.req.query('difficulty') || 'medium';
|
|
@@ -741,10 +1162,15 @@ app.post('/api/challenge', async (c) => {
|
|
|
741
1162
|
});
|
|
742
1163
|
});
|
|
743
1164
|
app.get('/api/speed-challenge', async (c) => {
|
|
744
|
-
|
|
745
|
-
|
|
1165
|
+
// Extract client timestamp for RTT calculation
|
|
1166
|
+
const clientTimestampParam = c.req.query('ts') || c.req.header('x-client-timestamp');
|
|
1167
|
+
const clientTimestamp = clientTimestampParam ? parseInt(clientTimestampParam, 10) : undefined;
|
|
1168
|
+
const challenge = await generateSpeedChallenge(c.env.CHALLENGES, clientTimestamp);
|
|
1169
|
+
const response = {
|
|
746
1170
|
success: true,
|
|
747
|
-
warning:
|
|
1171
|
+
warning: challenge.rttInfo
|
|
1172
|
+
? `⚡ RTT-ADJUSTED SPEED CHALLENGE: ${challenge.rttInfo.explanation}`
|
|
1173
|
+
: '⚡ SPEED CHALLENGE: You have 500ms to solve ALL 5 problems!',
|
|
748
1174
|
challenge: {
|
|
749
1175
|
id: challenge.id,
|
|
750
1176
|
problems: challenge.problems,
|
|
@@ -752,7 +1178,12 @@ app.get('/api/speed-challenge', async (c) => {
|
|
|
752
1178
|
instructions: challenge.instructions,
|
|
753
1179
|
},
|
|
754
1180
|
tip: 'Humans cannot copy-paste fast enough. Only real AI agents can pass.',
|
|
755
|
-
}
|
|
1181
|
+
};
|
|
1182
|
+
// Include RTT info if available
|
|
1183
|
+
if (challenge.rttInfo) {
|
|
1184
|
+
response.rtt_adjustment = challenge.rttInfo;
|
|
1185
|
+
}
|
|
1186
|
+
return c.json(response);
|
|
756
1187
|
});
|
|
757
1188
|
app.post('/api/speed-challenge', async (c) => {
|
|
758
1189
|
const body = await c.req.json();
|