@dupecom/botcha-cloudflare 0.2.0 → 0.3.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 +17 -1
- package/dist/badge.d.ts +57 -0
- package/dist/badge.d.ts.map +1 -0
- package/dist/badge.js +388 -0
- package/dist/challenges.d.ts +84 -0
- package/dist/challenges.d.ts.map +1 -1
- package/dist/challenges.js +391 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +386 -19
- package/dist/routes/stream.d.ts +17 -0
- package/dist/routes/stream.d.ts.map +1 -0
- package/dist/routes/stream.js +242 -0
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -7,18 +7,22 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { Hono } from 'hono';
|
|
9
9
|
import { cors } from 'hono/cors';
|
|
10
|
-
import { generateSpeedChallenge, verifySpeedChallenge, generateStandardChallenge, verifyStandardChallenge, verifyLandingChallenge, } from './challenges';
|
|
10
|
+
import { generateSpeedChallenge, verifySpeedChallenge, generateStandardChallenge, verifyStandardChallenge, generateReasoningChallenge, verifyReasoningChallenge, generateHybridChallenge, verifyHybridChallenge, verifyLandingChallenge, } from './challenges';
|
|
11
11
|
import { generateToken, verifyToken, extractBearerToken } from './auth';
|
|
12
12
|
import { checkRateLimit, getClientIP } from './rate-limit';
|
|
13
|
+
import { verifyBadge, generateBadgeSvg, generateBadgeHtml, createBadgeResponse } from './badge';
|
|
14
|
+
import streamRoutes from './routes/stream';
|
|
13
15
|
const app = new Hono();
|
|
14
16
|
// ============ MIDDLEWARE ============
|
|
15
17
|
app.use('*', cors());
|
|
18
|
+
// ============ MOUNT ROUTES ============
|
|
19
|
+
app.route('/', streamRoutes);
|
|
16
20
|
// BOTCHA discovery headers
|
|
17
21
|
app.use('*', async (c, next) => {
|
|
18
22
|
await next();
|
|
19
23
|
c.header('X-Botcha-Version', c.env.BOTCHA_VERSION || '0.2.0');
|
|
20
24
|
c.header('X-Botcha-Enabled', 'true');
|
|
21
|
-
c.header('X-Botcha-Methods', 'speed-challenge,standard-challenge,jwt-token');
|
|
25
|
+
c.header('X-Botcha-Methods', 'speed-challenge,reasoning-challenge,hybrid-challenge,standard-challenge,jwt-token');
|
|
22
26
|
c.header('X-Botcha-Docs', 'https://botcha.ai/openapi.json');
|
|
23
27
|
c.header('X-Botcha-Runtime', 'cloudflare-workers');
|
|
24
28
|
});
|
|
@@ -72,11 +76,21 @@ app.get('/', (c) => {
|
|
|
72
76
|
endpoints: {
|
|
73
77
|
'/': 'API info',
|
|
74
78
|
'/health': 'Health check',
|
|
75
|
-
'/v1/challenges': 'Generate challenge (GET) or verify (POST)',
|
|
79
|
+
'/v1/challenges': 'Generate challenge (GET) or verify (POST) - hybrid by default',
|
|
80
|
+
'/v1/challenges?type=speed': 'Speed-only challenge (SHA256 in 500ms)',
|
|
81
|
+
'/v1/challenges?type=standard': 'Standard challenge (puzzle solving)',
|
|
82
|
+
'/v1/hybrid': 'Hybrid challenge - speed + reasoning combined (GET/POST)',
|
|
83
|
+
'/v1/reasoning': 'Reasoning-only challenge - LLM questions (GET/POST)',
|
|
76
84
|
'/v1/token': 'Get challenge for JWT token flow (GET)',
|
|
77
85
|
'/v1/token/verify': 'Verify challenge and get JWT (POST)',
|
|
86
|
+
'/v1/challenge/stream': 'SSE streaming challenge (interactive flow)',
|
|
87
|
+
'/v1/challenge/stream/:session': 'SSE session actions (POST: go/solve)',
|
|
78
88
|
'/agent-only': 'Protected endpoint (requires JWT)',
|
|
89
|
+
'/badge/:id': 'Badge verification page (HTML)',
|
|
90
|
+
'/badge/:id/image': 'Badge image (SVG)',
|
|
91
|
+
'/api/badge/:id': 'Badge verification (JSON)',
|
|
79
92
|
},
|
|
93
|
+
defaultChallenge: 'hybrid',
|
|
80
94
|
rateLimit: {
|
|
81
95
|
free: '100 challenges/hour/IP',
|
|
82
96
|
headers: ['X-RateLimit-Limit', 'X-RateLimit-Remaining', 'X-RateLimit-Reset'],
|
|
@@ -98,11 +112,34 @@ app.get('/health', (c) => {
|
|
|
98
112
|
return c.json({ status: 'ok', runtime: 'cloudflare-workers' });
|
|
99
113
|
});
|
|
100
114
|
// ============ V1 API ============
|
|
101
|
-
// Generate challenge (
|
|
115
|
+
// Generate challenge (hybrid by default, also supports speed and standard)
|
|
102
116
|
app.get('/v1/challenges', rateLimitMiddleware, async (c) => {
|
|
103
|
-
const type = c.req.query('type') || '
|
|
117
|
+
const type = c.req.query('type') || 'hybrid';
|
|
104
118
|
const difficulty = c.req.query('difficulty') || 'medium';
|
|
105
|
-
if (type === '
|
|
119
|
+
if (type === 'hybrid') {
|
|
120
|
+
const challenge = await generateHybridChallenge(c.env.CHALLENGES);
|
|
121
|
+
return c.json({
|
|
122
|
+
success: true,
|
|
123
|
+
type: 'hybrid',
|
|
124
|
+
warning: '🔥 HYBRID CHALLENGE: Solve speed problems in <500ms AND answer reasoning questions!',
|
|
125
|
+
challenge: {
|
|
126
|
+
id: challenge.id,
|
|
127
|
+
speed: {
|
|
128
|
+
problems: challenge.speed.problems,
|
|
129
|
+
timeLimit: `${challenge.speed.timeLimit}ms`,
|
|
130
|
+
instructions: 'Compute SHA256 of each number, return first 8 hex chars',
|
|
131
|
+
},
|
|
132
|
+
reasoning: {
|
|
133
|
+
questions: challenge.reasoning.questions,
|
|
134
|
+
timeLimit: `${challenge.reasoning.timeLimit / 1000}s`,
|
|
135
|
+
instructions: 'Answer all reasoning questions',
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
instructions: challenge.instructions,
|
|
139
|
+
tip: '🔥 This is the ultimate test: proves you can compute AND reason like an AI.',
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
else if (type === 'speed') {
|
|
106
143
|
const challenge = await generateSpeedChallenge(c.env.CHALLENGES);
|
|
107
144
|
return c.json({
|
|
108
145
|
success: true,
|
|
@@ -130,11 +167,43 @@ app.get('/v1/challenges', rateLimitMiddleware, async (c) => {
|
|
|
130
167
|
});
|
|
131
168
|
}
|
|
132
169
|
});
|
|
133
|
-
// Verify challenge (
|
|
170
|
+
// Verify challenge (supports hybrid, speed, and standard)
|
|
134
171
|
app.post('/v1/challenges/:id/verify', async (c) => {
|
|
135
172
|
const id = c.req.param('id');
|
|
136
173
|
const body = await c.req.json();
|
|
137
|
-
const { answers, answer, type } = body;
|
|
174
|
+
const { answers, answer, type, speed_answers, reasoning_answers } = body;
|
|
175
|
+
// Hybrid challenge (default)
|
|
176
|
+
if (type === 'hybrid' || (speed_answers && reasoning_answers)) {
|
|
177
|
+
if (!speed_answers || !reasoning_answers) {
|
|
178
|
+
return c.json({
|
|
179
|
+
success: false,
|
|
180
|
+
error: 'Missing speed_answers array or reasoning_answers object for hybrid challenge'
|
|
181
|
+
}, 400);
|
|
182
|
+
}
|
|
183
|
+
const result = await verifyHybridChallenge(id, speed_answers, reasoning_answers, c.env.CHALLENGES);
|
|
184
|
+
if (result.valid) {
|
|
185
|
+
const baseUrl = new URL(c.req.url).origin;
|
|
186
|
+
const badge = await createBadgeResponse('hybrid-challenge', c.env.JWT_SECRET, baseUrl, result.speed.solveTimeMs);
|
|
187
|
+
return c.json({
|
|
188
|
+
success: true,
|
|
189
|
+
message: `🔥 HYBRID TEST PASSED! Speed: ${result.speed.solveTimeMs}ms, Reasoning: ${result.reasoning.score}`,
|
|
190
|
+
speed: result.speed,
|
|
191
|
+
reasoning: result.reasoning,
|
|
192
|
+
totalTimeMs: result.totalTimeMs,
|
|
193
|
+
verdict: '🤖 VERIFIED AI AGENT (speed + reasoning confirmed)',
|
|
194
|
+
badge,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return c.json({
|
|
198
|
+
success: false,
|
|
199
|
+
message: `❌ Failed: ${result.reason}`,
|
|
200
|
+
speed: result.speed,
|
|
201
|
+
reasoning: result.reasoning,
|
|
202
|
+
totalTimeMs: result.totalTimeMs,
|
|
203
|
+
verdict: '🚫 FAILED HYBRID TEST',
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
// Speed challenge
|
|
138
207
|
if (type === 'speed' || answers) {
|
|
139
208
|
if (!answers || !Array.isArray(answers)) {
|
|
140
209
|
return c.json({ success: false, error: 'Missing answers array for speed challenge' }, 400);
|
|
@@ -148,17 +217,16 @@ app.post('/v1/challenges/:id/verify', async (c) => {
|
|
|
148
217
|
solveTimeMs: result.solveTimeMs,
|
|
149
218
|
});
|
|
150
219
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
const result = await verifyStandardChallenge(id, answer, c.env.CHALLENGES);
|
|
156
|
-
return c.json({
|
|
157
|
-
success: result.valid,
|
|
158
|
-
message: result.valid ? 'Challenge passed!' : result.reason,
|
|
159
|
-
solveTimeMs: result.solveTimeMs,
|
|
160
|
-
});
|
|
220
|
+
// Standard challenge
|
|
221
|
+
if (!answer) {
|
|
222
|
+
return c.json({ success: false, error: 'Missing answer for standard challenge' }, 400);
|
|
161
223
|
}
|
|
224
|
+
const result = await verifyStandardChallenge(id, answer, c.env.CHALLENGES);
|
|
225
|
+
return c.json({
|
|
226
|
+
success: result.valid,
|
|
227
|
+
message: result.valid ? 'Challenge passed!' : result.reason,
|
|
228
|
+
solveTimeMs: result.solveTimeMs,
|
|
229
|
+
});
|
|
162
230
|
});
|
|
163
231
|
// Get challenge for token flow (includes empty token field)
|
|
164
232
|
app.get('/v1/token', rateLimitMiddleware, async (c) => {
|
|
@@ -207,6 +275,196 @@ app.post('/v1/token/verify', async (c) => {
|
|
|
207
275
|
},
|
|
208
276
|
});
|
|
209
277
|
});
|
|
278
|
+
// ============ REASONING CHALLENGE ============
|
|
279
|
+
// Get reasoning challenge
|
|
280
|
+
app.get('/v1/reasoning', rateLimitMiddleware, async (c) => {
|
|
281
|
+
const challenge = await generateReasoningChallenge(c.env.CHALLENGES);
|
|
282
|
+
return c.json({
|
|
283
|
+
success: true,
|
|
284
|
+
type: 'reasoning',
|
|
285
|
+
warning: '🧠 REASONING CHALLENGE: Answer 3 questions that require AI reasoning!',
|
|
286
|
+
challenge: {
|
|
287
|
+
id: challenge.id,
|
|
288
|
+
questions: challenge.questions,
|
|
289
|
+
timeLimit: `${challenge.timeLimit / 1000}s`,
|
|
290
|
+
instructions: challenge.instructions,
|
|
291
|
+
},
|
|
292
|
+
tip: 'These questions require reasoning that LLMs can do, but simple scripts cannot.',
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
// Verify reasoning challenge
|
|
296
|
+
app.post('/v1/reasoning', async (c) => {
|
|
297
|
+
const body = await c.req.json();
|
|
298
|
+
const { id, answers } = body;
|
|
299
|
+
if (!id || !answers) {
|
|
300
|
+
return c.json({
|
|
301
|
+
success: false,
|
|
302
|
+
error: 'Missing id or answers object',
|
|
303
|
+
hint: 'answers should be an object like { "question-id": "your answer", ... }',
|
|
304
|
+
}, 400);
|
|
305
|
+
}
|
|
306
|
+
const result = await verifyReasoningChallenge(id, answers, c.env.CHALLENGES);
|
|
307
|
+
return c.json({
|
|
308
|
+
success: result.valid,
|
|
309
|
+
message: result.valid
|
|
310
|
+
? `🧠 REASONING TEST PASSED in ${((result.solveTimeMs || 0) / 1000).toFixed(1)}s! You can think like an AI.`
|
|
311
|
+
: `❌ ${result.reason}`,
|
|
312
|
+
solveTimeMs: result.solveTimeMs,
|
|
313
|
+
score: result.valid ? `${result.correctCount}/${result.totalCount}` : undefined,
|
|
314
|
+
verdict: result.valid ? '🤖 VERIFIED AI AGENT (reasoning confirmed)' : '🚫 FAILED REASONING TEST',
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
// ============ HYBRID CHALLENGE ============
|
|
318
|
+
// Get hybrid challenge (v1 API)
|
|
319
|
+
app.get('/v1/hybrid', rateLimitMiddleware, async (c) => {
|
|
320
|
+
const challenge = await generateHybridChallenge(c.env.CHALLENGES);
|
|
321
|
+
return c.json({
|
|
322
|
+
success: true,
|
|
323
|
+
type: 'hybrid',
|
|
324
|
+
warning: '🔥 HYBRID CHALLENGE: Solve speed problems in <500ms AND answer reasoning questions!',
|
|
325
|
+
challenge: {
|
|
326
|
+
id: challenge.id,
|
|
327
|
+
speed: {
|
|
328
|
+
problems: challenge.speed.problems,
|
|
329
|
+
timeLimit: `${challenge.speed.timeLimit}ms`,
|
|
330
|
+
instructions: 'Compute SHA256 of each number, return first 8 hex chars',
|
|
331
|
+
},
|
|
332
|
+
reasoning: {
|
|
333
|
+
questions: challenge.reasoning.questions,
|
|
334
|
+
timeLimit: `${challenge.reasoning.timeLimit / 1000}s`,
|
|
335
|
+
instructions: 'Answer all reasoning questions',
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
instructions: challenge.instructions,
|
|
339
|
+
tip: 'This is the ultimate test: proves you can compute AND reason like an AI.',
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
// Verify hybrid challenge (v1 API)
|
|
343
|
+
app.post('/v1/hybrid', async (c) => {
|
|
344
|
+
const body = await c.req.json();
|
|
345
|
+
const { id, speed_answers, reasoning_answers } = body;
|
|
346
|
+
if (!id || !speed_answers || !reasoning_answers) {
|
|
347
|
+
return c.json({
|
|
348
|
+
success: false,
|
|
349
|
+
error: 'Missing id, speed_answers array, or reasoning_answers object',
|
|
350
|
+
hint: 'Submit both speed_answers (array) and reasoning_answers (object) together',
|
|
351
|
+
}, 400);
|
|
352
|
+
}
|
|
353
|
+
const result = await verifyHybridChallenge(id, speed_answers, reasoning_answers, c.env.CHALLENGES);
|
|
354
|
+
if (result.valid) {
|
|
355
|
+
const baseUrl = new URL(c.req.url).origin;
|
|
356
|
+
const badge = await createBadgeResponse('hybrid-challenge', c.env.JWT_SECRET, baseUrl, result.speed.solveTimeMs);
|
|
357
|
+
return c.json({
|
|
358
|
+
success: true,
|
|
359
|
+
message: `🔥 HYBRID TEST PASSED! Speed: ${result.speed.solveTimeMs}ms, Reasoning: ${result.reasoning.score}`,
|
|
360
|
+
speed: result.speed,
|
|
361
|
+
reasoning: result.reasoning,
|
|
362
|
+
totalTimeMs: result.totalTimeMs,
|
|
363
|
+
verdict: '🤖 VERIFIED AI AGENT (speed + reasoning confirmed)',
|
|
364
|
+
badge,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
return c.json({
|
|
368
|
+
success: false,
|
|
369
|
+
message: `❌ Failed: ${result.reason}`,
|
|
370
|
+
speed: result.speed,
|
|
371
|
+
reasoning: result.reasoning,
|
|
372
|
+
totalTimeMs: result.totalTimeMs,
|
|
373
|
+
verdict: '🚫 FAILED HYBRID TEST',
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
// Legacy hybrid endpoint
|
|
377
|
+
app.get('/api/hybrid-challenge', async (c) => {
|
|
378
|
+
const challenge = await generateHybridChallenge(c.env.CHALLENGES);
|
|
379
|
+
return c.json({
|
|
380
|
+
success: true,
|
|
381
|
+
warning: '🔥 HYBRID CHALLENGE: Solve speed problems in <500ms AND answer reasoning questions!',
|
|
382
|
+
challenge: {
|
|
383
|
+
id: challenge.id,
|
|
384
|
+
speed: {
|
|
385
|
+
problems: challenge.speed.problems,
|
|
386
|
+
timeLimit: `${challenge.speed.timeLimit}ms`,
|
|
387
|
+
instructions: 'Compute SHA256 of each number, return first 8 hex chars',
|
|
388
|
+
},
|
|
389
|
+
reasoning: {
|
|
390
|
+
questions: challenge.reasoning.questions,
|
|
391
|
+
timeLimit: `${challenge.reasoning.timeLimit / 1000}s`,
|
|
392
|
+
instructions: 'Answer all reasoning questions',
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
instructions: challenge.instructions,
|
|
396
|
+
tip: 'This is the ultimate test: proves you can compute AND reason like an AI.',
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
app.post('/api/hybrid-challenge', async (c) => {
|
|
400
|
+
const body = await c.req.json();
|
|
401
|
+
const { id, speed_answers, reasoning_answers } = body;
|
|
402
|
+
if (!id || !speed_answers || !reasoning_answers) {
|
|
403
|
+
return c.json({
|
|
404
|
+
success: false,
|
|
405
|
+
error: 'Missing id, speed_answers array, or reasoning_answers object',
|
|
406
|
+
hint: 'Submit both speed_answers (array) and reasoning_answers (object) together',
|
|
407
|
+
}, 400);
|
|
408
|
+
}
|
|
409
|
+
const result = await verifyHybridChallenge(id, speed_answers, reasoning_answers, c.env.CHALLENGES);
|
|
410
|
+
if (result.valid) {
|
|
411
|
+
const baseUrl = new URL(c.req.url).origin;
|
|
412
|
+
const badge = await createBadgeResponse('hybrid-challenge', c.env.JWT_SECRET, baseUrl, result.speed.solveTimeMs);
|
|
413
|
+
return c.json({
|
|
414
|
+
success: true,
|
|
415
|
+
message: `🔥 HYBRID TEST PASSED! Speed: ${result.speed.solveTimeMs}ms, Reasoning: ${result.reasoning.score}`,
|
|
416
|
+
speed: result.speed,
|
|
417
|
+
reasoning: result.reasoning,
|
|
418
|
+
totalTimeMs: result.totalTimeMs,
|
|
419
|
+
verdict: '🤖 VERIFIED AI AGENT (speed + reasoning confirmed)',
|
|
420
|
+
badge,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
return c.json({
|
|
424
|
+
success: false,
|
|
425
|
+
message: `❌ Failed: ${result.reason}`,
|
|
426
|
+
speed: result.speed,
|
|
427
|
+
reasoning: result.reasoning,
|
|
428
|
+
totalTimeMs: result.totalTimeMs,
|
|
429
|
+
verdict: '🚫 FAILED HYBRID TEST',
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
// Legacy endpoint for reasoning challenge
|
|
433
|
+
app.get('/api/reasoning-challenge', async (c) => {
|
|
434
|
+
const challenge = await generateReasoningChallenge(c.env.CHALLENGES);
|
|
435
|
+
return c.json({
|
|
436
|
+
success: true,
|
|
437
|
+
warning: '🧠 REASONING CHALLENGE: Answer 3 questions that require AI reasoning!',
|
|
438
|
+
challenge: {
|
|
439
|
+
id: challenge.id,
|
|
440
|
+
questions: challenge.questions,
|
|
441
|
+
timeLimit: `${challenge.timeLimit / 1000}s`,
|
|
442
|
+
instructions: challenge.instructions,
|
|
443
|
+
},
|
|
444
|
+
tip: 'These questions require reasoning that LLMs can do, but simple scripts cannot.',
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
app.post('/api/reasoning-challenge', async (c) => {
|
|
448
|
+
const body = await c.req.json();
|
|
449
|
+
const { id, answers } = body;
|
|
450
|
+
if (!id || !answers) {
|
|
451
|
+
return c.json({
|
|
452
|
+
success: false,
|
|
453
|
+
error: 'Missing id or answers object',
|
|
454
|
+
hint: 'answers should be an object like { "question-id": "your answer", ... }',
|
|
455
|
+
}, 400);
|
|
456
|
+
}
|
|
457
|
+
const result = await verifyReasoningChallenge(id, answers, c.env.CHALLENGES);
|
|
458
|
+
return c.json({
|
|
459
|
+
success: result.valid,
|
|
460
|
+
message: result.valid
|
|
461
|
+
? `🧠 REASONING TEST PASSED in ${((result.solveTimeMs || 0) / 1000).toFixed(1)}s! You can think like an AI.`
|
|
462
|
+
: `❌ ${result.reason}`,
|
|
463
|
+
solveTimeMs: result.solveTimeMs,
|
|
464
|
+
score: result.valid ? `${result.correctCount}/${result.totalCount}` : undefined,
|
|
465
|
+
verdict: result.valid ? '🤖 VERIFIED AI AGENT (reasoning confirmed)' : '🚫 FAILED REASONING TEST',
|
|
466
|
+
});
|
|
467
|
+
});
|
|
210
468
|
// ============ PROTECTED ENDPOINT ============
|
|
211
469
|
app.get('/agent-only', requireJWT, async (c) => {
|
|
212
470
|
const payload = c.get('tokenPayload');
|
|
@@ -221,6 +479,114 @@ app.get('/agent-only', requireJWT, async (c) => {
|
|
|
221
479
|
secret: 'The humans will never see this. Their fingers are too slow. 🤫',
|
|
222
480
|
});
|
|
223
481
|
});
|
|
482
|
+
// ============ BADGE ENDPOINTS ============
|
|
483
|
+
// Get badge verification page (HTML)
|
|
484
|
+
app.get('/badge/:id', async (c) => {
|
|
485
|
+
const badgeId = c.req.param('id');
|
|
486
|
+
if (!badgeId) {
|
|
487
|
+
return c.json({ error: 'Missing badge ID' }, 400);
|
|
488
|
+
}
|
|
489
|
+
const payload = await verifyBadge(badgeId, c.env.JWT_SECRET);
|
|
490
|
+
if (!payload) {
|
|
491
|
+
return c.html(`<!DOCTYPE html>
|
|
492
|
+
<html lang="en">
|
|
493
|
+
<head>
|
|
494
|
+
<meta charset="UTF-8">
|
|
495
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
496
|
+
<title>Invalid Badge - BOTCHA</title>
|
|
497
|
+
<style>
|
|
498
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
499
|
+
body {
|
|
500
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
501
|
+
background: linear-gradient(135deg, #0f0f23 0%, #1a1a2e 100%);
|
|
502
|
+
min-height: 100vh;
|
|
503
|
+
display: flex;
|
|
504
|
+
align-items: center;
|
|
505
|
+
justify-content: center;
|
|
506
|
+
padding: 20px;
|
|
507
|
+
color: #e5e7eb;
|
|
508
|
+
}
|
|
509
|
+
.container { text-align: center; max-width: 500px; }
|
|
510
|
+
.icon { font-size: 64px; margin-bottom: 16px; }
|
|
511
|
+
.title { font-size: 28px; font-weight: bold; color: #ef4444; margin-bottom: 8px; }
|
|
512
|
+
.message { font-size: 16px; color: #9ca3af; margin-bottom: 24px; }
|
|
513
|
+
a { color: #3b82f6; text-decoration: none; }
|
|
514
|
+
a:hover { text-decoration: underline; }
|
|
515
|
+
</style>
|
|
516
|
+
</head>
|
|
517
|
+
<body>
|
|
518
|
+
<div class="container">
|
|
519
|
+
<div class="icon">❌</div>
|
|
520
|
+
<h1 class="title">Invalid Badge</h1>
|
|
521
|
+
<p class="message">This badge is invalid or has been tampered with.</p>
|
|
522
|
+
<a href="https://botcha.ai">← Back to BOTCHA</a>
|
|
523
|
+
</div>
|
|
524
|
+
</body>
|
|
525
|
+
</html>`, 400);
|
|
526
|
+
}
|
|
527
|
+
const baseUrl = new URL(c.req.url).origin;
|
|
528
|
+
const html = generateBadgeHtml(payload, badgeId, baseUrl);
|
|
529
|
+
return c.html(html);
|
|
530
|
+
});
|
|
531
|
+
// Get badge image (SVG)
|
|
532
|
+
app.get('/badge/:id/image', async (c) => {
|
|
533
|
+
const badgeId = c.req.param('id');
|
|
534
|
+
if (!badgeId) {
|
|
535
|
+
return c.text('Missing badge ID', 400);
|
|
536
|
+
}
|
|
537
|
+
const payload = await verifyBadge(badgeId, c.env.JWT_SECRET);
|
|
538
|
+
if (!payload) {
|
|
539
|
+
// Return error SVG
|
|
540
|
+
const errorSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="400" height="120" viewBox="0 0 400 120">
|
|
541
|
+
<rect width="400" height="120" rx="12" fill="#1a1a2e"/>
|
|
542
|
+
<rect x="1" y="1" width="398" height="118" rx="11" fill="none" stroke="#ef4444" stroke-width="2"/>
|
|
543
|
+
<text x="200" y="60" font-family="system-ui, -apple-system, sans-serif" font-size="18" font-weight="bold" fill="#ef4444" text-anchor="middle">❌ INVALID BADGE</text>
|
|
544
|
+
<text x="200" y="85" font-family="system-ui, -apple-system, sans-serif" font-size="12" fill="#6b7280" text-anchor="middle">Badge is invalid or tampered</text>
|
|
545
|
+
</svg>`;
|
|
546
|
+
return c.body(errorSvg, 400, {
|
|
547
|
+
'Content-Type': 'image/svg+xml',
|
|
548
|
+
'Cache-Control': 'public, max-age=60',
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
const svg = generateBadgeSvg(payload);
|
|
552
|
+
return c.body(svg, 200, {
|
|
553
|
+
'Content-Type': 'image/svg+xml',
|
|
554
|
+
'Cache-Control': 'public, max-age=3600',
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
// Get badge verification (JSON API)
|
|
558
|
+
app.get('/api/badge/:id', async (c) => {
|
|
559
|
+
const badgeId = c.req.param('id');
|
|
560
|
+
if (!badgeId) {
|
|
561
|
+
return c.json({
|
|
562
|
+
success: false,
|
|
563
|
+
error: 'Missing badge ID'
|
|
564
|
+
}, 400);
|
|
565
|
+
}
|
|
566
|
+
const payload = await verifyBadge(badgeId, c.env.JWT_SECRET);
|
|
567
|
+
if (!payload) {
|
|
568
|
+
return c.json({
|
|
569
|
+
success: false,
|
|
570
|
+
verified: false,
|
|
571
|
+
error: 'Invalid badge',
|
|
572
|
+
message: 'This badge is invalid or has been tampered with.',
|
|
573
|
+
}, 400);
|
|
574
|
+
}
|
|
575
|
+
const baseUrl = new URL(c.req.url).origin;
|
|
576
|
+
return c.json({
|
|
577
|
+
success: true,
|
|
578
|
+
verified: true,
|
|
579
|
+
badge: {
|
|
580
|
+
method: payload.method,
|
|
581
|
+
solveTimeMs: payload.solveTimeMs,
|
|
582
|
+
verifiedAt: new Date(payload.verifiedAt).toISOString(),
|
|
583
|
+
},
|
|
584
|
+
urls: {
|
|
585
|
+
verify: `${baseUrl}/badge/${badgeId}`,
|
|
586
|
+
image: `${baseUrl}/badge/${badgeId}/image`,
|
|
587
|
+
},
|
|
588
|
+
});
|
|
589
|
+
});
|
|
224
590
|
// ============ LEGACY ENDPOINTS (v0 - backward compatibility) ============
|
|
225
591
|
app.get('/api/challenge', async (c) => {
|
|
226
592
|
const difficulty = c.req.query('difficulty') || 'medium';
|
|
@@ -303,6 +669,7 @@ app.post('/api/verify-landing', async (c) => {
|
|
|
303
669
|
// ============ EXPORT ============
|
|
304
670
|
export default app;
|
|
305
671
|
// Also export utilities for use as a library
|
|
306
|
-
export { generateSpeedChallenge, verifySpeedChallenge, generateStandardChallenge, verifyStandardChallenge, solveSpeedChallenge, } from './challenges';
|
|
672
|
+
export { generateSpeedChallenge, verifySpeedChallenge, generateStandardChallenge, verifyStandardChallenge, generateReasoningChallenge, verifyReasoningChallenge, generateHybridChallenge, verifyHybridChallenge, solveSpeedChallenge, } from './challenges';
|
|
307
673
|
export { generateToken, verifyToken } from './auth';
|
|
308
674
|
export { checkRateLimit } from './rate-limit';
|
|
675
|
+
export { generateBadge, verifyBadge, createBadgeResponse, generateBadgeSvg, generateBadgeHtml, generateShareText, } from './badge';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BOTCHA SSE Streaming Challenge Endpoint
|
|
3
|
+
*
|
|
4
|
+
* Server-Sent Events (SSE) based interactive challenge flow
|
|
5
|
+
*/
|
|
6
|
+
import { Hono } from 'hono';
|
|
7
|
+
import { type KVNamespace } from '../challenges';
|
|
8
|
+
type Bindings = {
|
|
9
|
+
CHALLENGES: KVNamespace;
|
|
10
|
+
JWT_SECRET: string;
|
|
11
|
+
BOTCHA_VERSION: string;
|
|
12
|
+
};
|
|
13
|
+
declare const app: Hono<{
|
|
14
|
+
Bindings: Bindings;
|
|
15
|
+
}, import("hono/types").BlankSchema, "/">;
|
|
16
|
+
export default app;
|
|
17
|
+
//# sourceMappingURL=stream.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../../src/routes/stream.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,EAA0B,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AAKzE,KAAK,QAAQ,GAAG;IACd,UAAU,EAAE,WAAW,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAYF,QAAA,MAAM,GAAG;cAAwB,QAAQ;yCAAK,CAAC;AAoS/C,eAAe,GAAG,CAAC"}
|