@dupecom/botcha-cloudflare 0.2.1 → 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 +15 -0
- package/dist/badge.d.ts +1 -1
- package/dist/badge.d.ts.map +1 -1
- package/dist/badge.js +18 -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 +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +274 -20
- 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 +3 -3
package/README.md
CHANGED
|
@@ -96,8 +96,23 @@ Rate limit headers:
|
|
|
96
96
|
| `/v1/challenges/:id/verify` | POST | Verify challenge (no JWT) |
|
|
97
97
|
| `/v1/token` | GET | Get challenge for JWT flow |
|
|
98
98
|
| `/v1/token/verify` | POST | Verify challenge → get JWT token |
|
|
99
|
+
| `/v1/challenge/stream` | GET | SSE streaming challenge (AI-native) |
|
|
100
|
+
| `/v1/challenge/stream/:session` | POST | SSE action handler (go, solve) |
|
|
99
101
|
| `/agent-only` | GET | Protected endpoint (requires JWT) |
|
|
100
102
|
|
|
103
|
+
### SSE Streaming (AI-Native)
|
|
104
|
+
|
|
105
|
+
For AI agents that prefer conversational flows, BOTCHA offers Server-Sent Events streaming:
|
|
106
|
+
|
|
107
|
+
**Flow:**
|
|
108
|
+
1. `GET /v1/challenge/stream` - Opens SSE connection, receive welcome/instructions/ready events
|
|
109
|
+
2. `POST /v1/challenge/stream/:session` with `{action:"go"}` - Start challenge timer (fair timing!)
|
|
110
|
+
3. Receive `challenge` event with problems
|
|
111
|
+
4. `POST /v1/challenge/stream/:session` with `{action:"solve", answers:[...]}` - Submit solution
|
|
112
|
+
5. Receive `result` event with JWT token
|
|
113
|
+
|
|
114
|
+
**Benefits:** Timer starts when you say "GO" (not on connection), natural back-and-forth handshake.
|
|
115
|
+
|
|
101
116
|
### Legacy API (v0 - backward compatible)
|
|
102
117
|
|
|
103
118
|
| Endpoint | Method | Description |
|
package/dist/badge.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Port of badge generation and verification logic from src/utils/badge.ts
|
|
5
5
|
* Uses jose library for HMAC-SHA256 signing (same as JWT auth)
|
|
6
6
|
*/
|
|
7
|
-
export type BadgeMethod = 'speed-challenge' | 'landing-challenge' | 'standard-challenge' | 'web-bot-auth';
|
|
7
|
+
export type BadgeMethod = 'speed-challenge' | 'landing-challenge' | 'standard-challenge' | 'web-bot-auth' | 'reasoning-challenge' | 'hybrid-challenge';
|
|
8
8
|
export interface BadgePayload {
|
|
9
9
|
method: BadgeMethod;
|
|
10
10
|
solveTimeMs?: number;
|
package/dist/badge.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"badge.d.ts","sourceRoot":"","sources":["../src/badge.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,MAAM,MAAM,WAAW,GAAG,iBAAiB,GAAG,mBAAmB,GAAG,oBAAoB,GAAG,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"badge.d.ts","sourceRoot":"","sources":["../src/badge.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,MAAM,MAAM,WAAW,GAAG,iBAAiB,GAAG,mBAAmB,GAAG,oBAAoB,GAAG,cAAc,GAAG,qBAAqB,GAAG,kBAAkB,CAAC;AAEvJ,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,WAAW,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,YAAY,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE;QACJ,MAAM,EAAE,WAAW,CAAC;QACpB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;CACH;AAID;;;GAGG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAgB1F;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAuB9B;AAID;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,GAAG,YAAY,CA6DvG;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,KAAK,CAAC,CAqBhB;AA+BD;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,YAAY,EACrB,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GAChD,MAAM,CAuER;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAyJjG"}
|
package/dist/badge.js
CHANGED
|
@@ -78,6 +78,18 @@ export function generateShareText(badgeId, payload, baseUrl) {
|
|
|
78
78
|
title: 'I verified via BOTCHA Web Bot Auth!',
|
|
79
79
|
subtitle: 'Cryptographic identity confirmed.',
|
|
80
80
|
},
|
|
81
|
+
'reasoning-challenge': {
|
|
82
|
+
title: payload.solveTimeMs
|
|
83
|
+
? `I passed the BOTCHA reasoning test in ${(payload.solveTimeMs / 1000).toFixed(1)}s!`
|
|
84
|
+
: 'I passed the BOTCHA reasoning test!',
|
|
85
|
+
subtitle: 'Proved I can reason like an AI.',
|
|
86
|
+
},
|
|
87
|
+
'hybrid-challenge': {
|
|
88
|
+
title: payload.solveTimeMs
|
|
89
|
+
? `I passed the BOTCHA hybrid test in ${(payload.solveTimeMs / 1000).toFixed(1)}s!`
|
|
90
|
+
: 'I passed the BOTCHA hybrid test!',
|
|
91
|
+
subtitle: 'Proved I can compute AND reason like an AI.',
|
|
92
|
+
},
|
|
81
93
|
};
|
|
82
94
|
const desc = methodDescriptions[payload.method];
|
|
83
95
|
const twitter = `${desc.title}
|
|
@@ -126,18 +138,24 @@ const METHOD_COLORS = {
|
|
|
126
138
|
'landing-challenge': { bg: '#1a1a2e', accent: '#10b981', text: '#d1fae5' },
|
|
127
139
|
'standard-challenge': { bg: '#1a1a2e', accent: '#3b82f6', text: '#dbeafe' },
|
|
128
140
|
'web-bot-auth': { bg: '#1a1a2e', accent: '#8b5cf6', text: '#ede9fe' },
|
|
141
|
+
'reasoning-challenge': { bg: '#1a1a2e', accent: '#ec4899', text: '#fce7f3' },
|
|
142
|
+
'hybrid-challenge': { bg: '#1a1a2e', accent: '#ef4444', text: '#fecaca' },
|
|
129
143
|
};
|
|
130
144
|
const METHOD_LABELS = {
|
|
131
145
|
'speed-challenge': 'SPEED TEST',
|
|
132
146
|
'landing-challenge': 'LANDING CHALLENGE',
|
|
133
147
|
'standard-challenge': 'CHALLENGE',
|
|
134
148
|
'web-bot-auth': 'WEB BOT AUTH',
|
|
149
|
+
'reasoning-challenge': 'REASONING TEST',
|
|
150
|
+
'hybrid-challenge': 'HYBRID TEST',
|
|
135
151
|
};
|
|
136
152
|
const METHOD_ICONS = {
|
|
137
153
|
'speed-challenge': '⚡',
|
|
138
154
|
'landing-challenge': '🌐',
|
|
139
155
|
'standard-challenge': '🔢',
|
|
140
156
|
'web-bot-auth': '🔐',
|
|
157
|
+
'reasoning-challenge': '🧠',
|
|
158
|
+
'hybrid-challenge': '🔥',
|
|
141
159
|
};
|
|
142
160
|
/**
|
|
143
161
|
* Generate an SVG badge image
|
package/dist/challenges.d.ts
CHANGED
|
@@ -28,10 +28,36 @@ export interface StandardChallenge {
|
|
|
28
28
|
expiresAt: number;
|
|
29
29
|
difficulty: 'easy' | 'medium' | 'hard';
|
|
30
30
|
}
|
|
31
|
+
export interface ReasoningQuestion {
|
|
32
|
+
id: string;
|
|
33
|
+
question: string;
|
|
34
|
+
category: 'analogy' | 'logic' | 'wordplay' | 'math' | 'code' | 'common-sense';
|
|
35
|
+
acceptedAnswers: string[];
|
|
36
|
+
}
|
|
37
|
+
export interface ReasoningChallenge {
|
|
38
|
+
id: string;
|
|
39
|
+
questions: {
|
|
40
|
+
id: string;
|
|
41
|
+
question: string;
|
|
42
|
+
category: string;
|
|
43
|
+
}[];
|
|
44
|
+
expectedAnswers: Record<string, string[]>;
|
|
45
|
+
issuedAt: number;
|
|
46
|
+
expiresAt: number;
|
|
47
|
+
}
|
|
31
48
|
export interface ChallengeResult {
|
|
32
49
|
valid: boolean;
|
|
33
50
|
reason?: string;
|
|
34
51
|
solveTimeMs?: number;
|
|
52
|
+
correctCount?: number;
|
|
53
|
+
totalCount?: number;
|
|
54
|
+
}
|
|
55
|
+
export interface HybridChallenge {
|
|
56
|
+
id: string;
|
|
57
|
+
speedChallengeId: string;
|
|
58
|
+
reasoningChallengeId: string;
|
|
59
|
+
issuedAt: number;
|
|
60
|
+
expiresAt: number;
|
|
35
61
|
}
|
|
36
62
|
/**
|
|
37
63
|
* Generate a speed challenge: 5 SHA256 problems, 500ms to solve ALL
|
|
@@ -82,4 +108,62 @@ export declare function validateLandingToken(token: string, kv?: KVNamespace): P
|
|
|
82
108
|
* Solve speed challenge problems (utility for AI agents)
|
|
83
109
|
*/
|
|
84
110
|
export declare function solveSpeedChallenge(problems: number[]): Promise<string[]>;
|
|
111
|
+
/**
|
|
112
|
+
* Generate a reasoning challenge: 3 random questions requiring LLM capabilities
|
|
113
|
+
*/
|
|
114
|
+
export declare function generateReasoningChallenge(kv?: KVNamespace): Promise<{
|
|
115
|
+
id: string;
|
|
116
|
+
questions: {
|
|
117
|
+
id: string;
|
|
118
|
+
question: string;
|
|
119
|
+
category: string;
|
|
120
|
+
}[];
|
|
121
|
+
timeLimit: number;
|
|
122
|
+
instructions: string;
|
|
123
|
+
}>;
|
|
124
|
+
/**
|
|
125
|
+
* Verify a reasoning challenge response
|
|
126
|
+
*/
|
|
127
|
+
export declare function verifyReasoningChallenge(id: string, answers: Record<string, string>, kv?: KVNamespace): Promise<ChallengeResult>;
|
|
128
|
+
/**
|
|
129
|
+
* Generate a hybrid challenge: speed + reasoning combined
|
|
130
|
+
*/
|
|
131
|
+
export declare function generateHybridChallenge(kv?: KVNamespace): Promise<{
|
|
132
|
+
id: string;
|
|
133
|
+
speed: {
|
|
134
|
+
problems: {
|
|
135
|
+
num: number;
|
|
136
|
+
operation: string;
|
|
137
|
+
}[];
|
|
138
|
+
timeLimit: number;
|
|
139
|
+
};
|
|
140
|
+
reasoning: {
|
|
141
|
+
questions: {
|
|
142
|
+
id: string;
|
|
143
|
+
question: string;
|
|
144
|
+
category: string;
|
|
145
|
+
}[];
|
|
146
|
+
timeLimit: number;
|
|
147
|
+
};
|
|
148
|
+
instructions: string;
|
|
149
|
+
}>;
|
|
150
|
+
/**
|
|
151
|
+
* Verify a hybrid challenge response
|
|
152
|
+
*/
|
|
153
|
+
export declare function verifyHybridChallenge(id: string, speedAnswers: string[], reasoningAnswers: Record<string, string>, kv?: KVNamespace): Promise<{
|
|
154
|
+
valid: boolean;
|
|
155
|
+
reason?: string;
|
|
156
|
+
speed: {
|
|
157
|
+
passed: boolean;
|
|
158
|
+
solveTimeMs?: number;
|
|
159
|
+
reason?: string;
|
|
160
|
+
};
|
|
161
|
+
reasoning: {
|
|
162
|
+
passed: boolean;
|
|
163
|
+
score?: string;
|
|
164
|
+
solveTimeMs?: number;
|
|
165
|
+
reason?: string;
|
|
166
|
+
};
|
|
167
|
+
totalTimeMs?: number;
|
|
168
|
+
}>;
|
|
85
169
|
//# sourceMappingURL=challenges.d.ts.map
|
package/dist/challenges.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"challenges.d.ts","sourceRoot":"","sources":["../src/challenges.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,MAAM,MAAM,WAAW,GAAG;IACxB,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,aAAa,GAAG,QAAQ,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;IACtF,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzF,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AAGF,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC/C,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;CACxC;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"challenges.d.ts","sourceRoot":"","sources":["../src/challenges.ts"],"names":[],"mappings":"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"}
|
package/dist/challenges.js
CHANGED
|
@@ -246,3 +246,394 @@ export async function validateLandingToken(token, kv) {
|
|
|
246
246
|
export async function solveSpeedChallenge(problems) {
|
|
247
247
|
return Promise.all(problems.map(n => sha256First(n.toString(), 8)));
|
|
248
248
|
}
|
|
249
|
+
// ============ REASONING CHALLENGE ============
|
|
250
|
+
// In-memory storage for reasoning challenges
|
|
251
|
+
const reasoningChallenges = new Map();
|
|
252
|
+
// Question bank - LLMs can answer these, simple scripts cannot
|
|
253
|
+
const QUESTION_BANK = [
|
|
254
|
+
// Analogies
|
|
255
|
+
{
|
|
256
|
+
id: 'analogy-1',
|
|
257
|
+
question: 'Complete the analogy: Book is to library as car is to ___',
|
|
258
|
+
category: 'analogy',
|
|
259
|
+
acceptedAnswers: ['garage', 'parking lot', 'dealership', 'parking garage', 'lot'],
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
id: 'analogy-2',
|
|
263
|
+
question: 'Complete the analogy: Painter is to brush as writer is to ___',
|
|
264
|
+
category: 'analogy',
|
|
265
|
+
acceptedAnswers: ['pen', 'pencil', 'keyboard', 'typewriter', 'quill'],
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
id: 'analogy-3',
|
|
269
|
+
question: 'Complete the analogy: Fish is to water as bird is to ___',
|
|
270
|
+
category: 'analogy',
|
|
271
|
+
acceptedAnswers: ['air', 'sky', 'atmosphere'],
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
id: 'analogy-4',
|
|
275
|
+
question: 'Complete the analogy: Eye is to see as ear is to ___',
|
|
276
|
+
category: 'analogy',
|
|
277
|
+
acceptedAnswers: ['hear', 'listen', 'hearing', 'listening'],
|
|
278
|
+
},
|
|
279
|
+
// Wordplay
|
|
280
|
+
{
|
|
281
|
+
id: 'wordplay-1',
|
|
282
|
+
question: 'What single word connects: apple, Newton, gravity?',
|
|
283
|
+
category: 'wordplay',
|
|
284
|
+
acceptedAnswers: ['tree', 'fall', 'falling'],
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
id: 'wordplay-2',
|
|
288
|
+
question: 'What single word connects: key, piano, computer?',
|
|
289
|
+
category: 'wordplay',
|
|
290
|
+
acceptedAnswers: ['keyboard', 'board', 'keys'],
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
id: 'wordplay-3',
|
|
294
|
+
question: 'What single word connects: river, money, blood?',
|
|
295
|
+
category: 'wordplay',
|
|
296
|
+
acceptedAnswers: ['bank', 'flow', 'stream'],
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
id: 'wordplay-4',
|
|
300
|
+
question: 'What word can precede: light, house, shine?',
|
|
301
|
+
category: 'wordplay',
|
|
302
|
+
acceptedAnswers: ['sun', 'moon'],
|
|
303
|
+
},
|
|
304
|
+
// Logic
|
|
305
|
+
{
|
|
306
|
+
id: 'logic-1',
|
|
307
|
+
question: 'If all Bloops are Razzies and all Razzies are Lazzies, are all Bloops definitely Lazzies? Answer yes or no.',
|
|
308
|
+
category: 'logic',
|
|
309
|
+
acceptedAnswers: ['yes'],
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
id: 'logic-2',
|
|
313
|
+
question: 'If some Widgets are Gadgets, and all Gadgets are blue, can some Widgets be blue? Answer yes or no.',
|
|
314
|
+
category: 'logic',
|
|
315
|
+
acceptedAnswers: ['yes'],
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
id: 'logic-3',
|
|
319
|
+
question: 'I have a bee in my hand. What do I have in my eye? (Think about the saying)',
|
|
320
|
+
category: 'logic',
|
|
321
|
+
acceptedAnswers: ['beauty', 'beholder'],
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
id: 'logic-4',
|
|
325
|
+
question: 'A farmer has 17 sheep. All but 9 run away. How many sheep does he have left?',
|
|
326
|
+
category: 'logic',
|
|
327
|
+
acceptedAnswers: ['9', 'nine'],
|
|
328
|
+
},
|
|
329
|
+
// Math
|
|
330
|
+
{
|
|
331
|
+
id: 'math-1',
|
|
332
|
+
question: 'A bat and ball cost $1.10 total. The bat costs $1.00 more than the ball. How much does the ball cost in cents?',
|
|
333
|
+
category: 'math',
|
|
334
|
+
acceptedAnswers: ['5', '5 cents', 'five', 'five cents', '0.05', '$0.05'],
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
id: 'math-2',
|
|
338
|
+
question: 'If it takes 5 machines 5 minutes to make 5 widgets, how many minutes would it take 100 machines to make 100 widgets?',
|
|
339
|
+
category: 'math',
|
|
340
|
+
acceptedAnswers: ['5', 'five', '5 minutes', 'five minutes'],
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
id: 'math-3',
|
|
344
|
+
question: 'In a lake, there is a patch of lily pads. Every day, the patch doubles in size. If it takes 48 days for the patch to cover the entire lake, how many days would it take for the patch to cover half of the lake?',
|
|
345
|
+
category: 'math',
|
|
346
|
+
acceptedAnswers: ['47', 'forty-seven', 'forty seven', '47 days'],
|
|
347
|
+
},
|
|
348
|
+
// Code
|
|
349
|
+
{
|
|
350
|
+
id: 'code-1',
|
|
351
|
+
question: 'What is wrong with this code: if (x = 5) { doSomething(); }',
|
|
352
|
+
category: 'code',
|
|
353
|
+
acceptedAnswers: ['assignment', 'single equals', '= instead of ==', 'should be ==', 'should be ===', 'equality', 'comparison'],
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
id: 'code-2',
|
|
357
|
+
question: 'In most programming languages, what does the modulo operator % return for 17 % 5?',
|
|
358
|
+
category: 'code',
|
|
359
|
+
acceptedAnswers: ['2', 'two'],
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
id: 'code-3',
|
|
363
|
+
question: 'What data structure uses LIFO (Last In, First Out)?',
|
|
364
|
+
category: 'code',
|
|
365
|
+
acceptedAnswers: ['stack', 'a stack'],
|
|
366
|
+
},
|
|
367
|
+
// Common sense
|
|
368
|
+
{
|
|
369
|
+
id: 'sense-1',
|
|
370
|
+
question: 'If you are running a race and you pass the person in second place, what place are you in now?',
|
|
371
|
+
category: 'common-sense',
|
|
372
|
+
acceptedAnswers: ['second', '2nd', '2', 'two'],
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
id: 'sense-2',
|
|
376
|
+
question: 'What gets wetter the more it dries?',
|
|
377
|
+
category: 'common-sense',
|
|
378
|
+
acceptedAnswers: ['towel', 'a towel', 'cloth', 'rag'],
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
id: 'sense-3',
|
|
382
|
+
question: 'What can you catch but not throw?',
|
|
383
|
+
category: 'common-sense',
|
|
384
|
+
acceptedAnswers: ['cold', 'a cold', 'breath', 'your breath', 'feelings', 'disease'],
|
|
385
|
+
},
|
|
386
|
+
];
|
|
387
|
+
/**
|
|
388
|
+
* Generate a reasoning challenge: 3 random questions requiring LLM capabilities
|
|
389
|
+
*/
|
|
390
|
+
export async function generateReasoningChallenge(kv) {
|
|
391
|
+
cleanExpired();
|
|
392
|
+
const id = uuid();
|
|
393
|
+
// Pick 3 random questions from different categories
|
|
394
|
+
const shuffled = [...QUESTION_BANK].sort(() => Math.random() - 0.5);
|
|
395
|
+
const selectedCategories = new Set();
|
|
396
|
+
const selectedQuestions = [];
|
|
397
|
+
for (const q of shuffled) {
|
|
398
|
+
if (selectedQuestions.length >= 3)
|
|
399
|
+
break;
|
|
400
|
+
if (selectedQuestions.length < 2 || !selectedCategories.has(q.category)) {
|
|
401
|
+
selectedQuestions.push(q);
|
|
402
|
+
selectedCategories.add(q.category);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
while (selectedQuestions.length < 3 && shuffled.length > selectedQuestions.length) {
|
|
406
|
+
const q = shuffled.find(sq => !selectedQuestions.includes(sq));
|
|
407
|
+
if (q)
|
|
408
|
+
selectedQuestions.push(q);
|
|
409
|
+
}
|
|
410
|
+
const expectedAnswers = {};
|
|
411
|
+
const questions = selectedQuestions.map(q => {
|
|
412
|
+
expectedAnswers[q.id] = q.acceptedAnswers;
|
|
413
|
+
return {
|
|
414
|
+
id: q.id,
|
|
415
|
+
question: q.question,
|
|
416
|
+
category: q.category,
|
|
417
|
+
};
|
|
418
|
+
});
|
|
419
|
+
const timeLimit = 30000; // 30 seconds
|
|
420
|
+
const challenge = {
|
|
421
|
+
id,
|
|
422
|
+
questions,
|
|
423
|
+
expectedAnswers,
|
|
424
|
+
issuedAt: Date.now(),
|
|
425
|
+
expiresAt: Date.now() + timeLimit + 5000,
|
|
426
|
+
};
|
|
427
|
+
// Store in KV or memory
|
|
428
|
+
if (kv) {
|
|
429
|
+
await kv.put(`challenge:${id}`, JSON.stringify(challenge), { expirationTtl: 300 });
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
reasoningChallenges.set(id, challenge);
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
id,
|
|
436
|
+
questions,
|
|
437
|
+
timeLimit,
|
|
438
|
+
instructions: 'Answer all 3 questions. These require reasoning that LLMs can do but simple scripts cannot. You have 30 seconds.',
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Normalize answer for comparison
|
|
443
|
+
*/
|
|
444
|
+
function normalizeAnswer(answer) {
|
|
445
|
+
return answer
|
|
446
|
+
.toLowerCase()
|
|
447
|
+
.trim()
|
|
448
|
+
.replace(/[.,!?'"]/g, '')
|
|
449
|
+
.replace(/\s+/g, ' ');
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Check if an answer matches any accepted answer
|
|
453
|
+
*/
|
|
454
|
+
function isAnswerAccepted(answer, acceptedAnswers) {
|
|
455
|
+
const normalized = normalizeAnswer(answer);
|
|
456
|
+
for (const accepted of acceptedAnswers) {
|
|
457
|
+
const normalizedAccepted = normalizeAnswer(accepted);
|
|
458
|
+
if (normalized === normalizedAccepted)
|
|
459
|
+
return true;
|
|
460
|
+
if (normalized.includes(normalizedAccepted))
|
|
461
|
+
return true;
|
|
462
|
+
if (normalizedAccepted.includes(normalized) && normalized.length > 2)
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Verify a reasoning challenge response
|
|
469
|
+
*/
|
|
470
|
+
export async function verifyReasoningChallenge(id, answers, kv) {
|
|
471
|
+
let challenge = null;
|
|
472
|
+
if (kv) {
|
|
473
|
+
const data = await kv.get(`challenge:${id}`);
|
|
474
|
+
challenge = data ? JSON.parse(data) : null;
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
cleanExpired();
|
|
478
|
+
challenge = reasoningChallenges.get(id) || null;
|
|
479
|
+
}
|
|
480
|
+
if (!challenge) {
|
|
481
|
+
return { valid: false, reason: 'Challenge not found or expired' };
|
|
482
|
+
}
|
|
483
|
+
const now = Date.now();
|
|
484
|
+
const solveTimeMs = now - challenge.issuedAt;
|
|
485
|
+
// Delete challenge
|
|
486
|
+
if (kv) {
|
|
487
|
+
await kv.delete(`challenge:${id}`);
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
reasoningChallenges.delete(id);
|
|
491
|
+
}
|
|
492
|
+
if (now > challenge.expiresAt) {
|
|
493
|
+
return { valid: false, reason: `Too slow! Took ${solveTimeMs}ms, limit was 30 seconds` };
|
|
494
|
+
}
|
|
495
|
+
if (!answers || typeof answers !== 'object') {
|
|
496
|
+
return { valid: false, reason: 'Answers must be an object mapping question IDs to answers' };
|
|
497
|
+
}
|
|
498
|
+
let correctCount = 0;
|
|
499
|
+
const totalCount = challenge.questions.length;
|
|
500
|
+
const wrongQuestions = [];
|
|
501
|
+
for (const q of challenge.questions) {
|
|
502
|
+
const userAnswer = answers[q.id];
|
|
503
|
+
const acceptedAnswers = challenge.expectedAnswers[q.id] || [];
|
|
504
|
+
if (!userAnswer) {
|
|
505
|
+
wrongQuestions.push(q.id);
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
if (isAnswerAccepted(userAnswer, acceptedAnswers)) {
|
|
509
|
+
correctCount++;
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
wrongQuestions.push(q.id);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (correctCount < totalCount) {
|
|
516
|
+
return {
|
|
517
|
+
valid: false,
|
|
518
|
+
reason: `Only ${correctCount}/${totalCount} correct. Wrong: ${wrongQuestions.join(', ')}`,
|
|
519
|
+
solveTimeMs,
|
|
520
|
+
correctCount,
|
|
521
|
+
totalCount,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
return {
|
|
525
|
+
valid: true,
|
|
526
|
+
solveTimeMs,
|
|
527
|
+
correctCount,
|
|
528
|
+
totalCount,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
// ============ HYBRID CHALLENGE ============
|
|
532
|
+
const hybridChallenges = new Map();
|
|
533
|
+
/**
|
|
534
|
+
* Generate a hybrid challenge: speed + reasoning combined
|
|
535
|
+
*/
|
|
536
|
+
export async function generateHybridChallenge(kv) {
|
|
537
|
+
cleanExpired();
|
|
538
|
+
const id = uuid();
|
|
539
|
+
// Generate both sub-challenges
|
|
540
|
+
const speedChallenge = await generateSpeedChallenge(kv);
|
|
541
|
+
const reasoningChallenge = await generateReasoningChallenge(kv);
|
|
542
|
+
const hybrid = {
|
|
543
|
+
id,
|
|
544
|
+
speedChallengeId: speedChallenge.id,
|
|
545
|
+
reasoningChallengeId: reasoningChallenge.id,
|
|
546
|
+
issuedAt: Date.now(),
|
|
547
|
+
expiresAt: Date.now() + 35000,
|
|
548
|
+
};
|
|
549
|
+
// Store in KV or memory
|
|
550
|
+
if (kv) {
|
|
551
|
+
await kv.put(`hybrid:${id}`, JSON.stringify(hybrid), { expirationTtl: 300 });
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
hybridChallenges.set(id, hybrid);
|
|
555
|
+
}
|
|
556
|
+
return {
|
|
557
|
+
id,
|
|
558
|
+
speed: {
|
|
559
|
+
problems: speedChallenge.problems,
|
|
560
|
+
timeLimit: speedChallenge.timeLimit,
|
|
561
|
+
},
|
|
562
|
+
reasoning: {
|
|
563
|
+
questions: reasoningChallenge.questions,
|
|
564
|
+
timeLimit: reasoningChallenge.timeLimit,
|
|
565
|
+
},
|
|
566
|
+
instructions: 'Solve ALL speed problems (SHA256) in <500ms AND answer ALL reasoning questions. Submit both together.',
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Verify a hybrid challenge response
|
|
571
|
+
*/
|
|
572
|
+
export async function verifyHybridChallenge(id, speedAnswers, reasoningAnswers, kv) {
|
|
573
|
+
let hybrid = null;
|
|
574
|
+
if (kv) {
|
|
575
|
+
const data = await kv.get(`hybrid:${id}`);
|
|
576
|
+
hybrid = data ? JSON.parse(data) : null;
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
cleanExpired();
|
|
580
|
+
hybrid = hybridChallenges.get(id) || null;
|
|
581
|
+
}
|
|
582
|
+
if (!hybrid) {
|
|
583
|
+
return {
|
|
584
|
+
valid: false,
|
|
585
|
+
reason: 'Hybrid challenge not found or expired',
|
|
586
|
+
speed: { passed: false, reason: 'Challenge not found' },
|
|
587
|
+
reasoning: { passed: false, reason: 'Challenge not found' },
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
const now = Date.now();
|
|
591
|
+
const totalTimeMs = now - hybrid.issuedAt;
|
|
592
|
+
if (now > hybrid.expiresAt) {
|
|
593
|
+
if (kv) {
|
|
594
|
+
await kv.delete(`hybrid:${id}`);
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
hybridChallenges.delete(id);
|
|
598
|
+
}
|
|
599
|
+
return {
|
|
600
|
+
valid: false,
|
|
601
|
+
reason: 'Hybrid challenge expired',
|
|
602
|
+
speed: { passed: false, reason: 'Expired' },
|
|
603
|
+
reasoning: { passed: false, reason: 'Expired' },
|
|
604
|
+
totalTimeMs,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
// Verify speed challenge
|
|
608
|
+
const speedResult = await verifySpeedChallenge(hybrid.speedChallengeId, speedAnswers, kv);
|
|
609
|
+
// Verify reasoning challenge
|
|
610
|
+
const reasoningResult = await verifyReasoningChallenge(hybrid.reasoningChallengeId, reasoningAnswers, kv);
|
|
611
|
+
// Clean up hybrid
|
|
612
|
+
if (kv) {
|
|
613
|
+
await kv.delete(`hybrid:${id}`);
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
hybridChallenges.delete(id);
|
|
617
|
+
}
|
|
618
|
+
const speedPassed = speedResult.valid;
|
|
619
|
+
const reasoningPassed = reasoningResult.valid;
|
|
620
|
+
const bothPassed = speedPassed && reasoningPassed;
|
|
621
|
+
return {
|
|
622
|
+
valid: bothPassed,
|
|
623
|
+
reason: bothPassed
|
|
624
|
+
? undefined
|
|
625
|
+
: `Failed: ${!speedPassed ? 'speed' : ''}${!speedPassed && !reasoningPassed ? ' + ' : ''}${!reasoningPassed ? 'reasoning' : ''}`,
|
|
626
|
+
speed: {
|
|
627
|
+
passed: speedPassed,
|
|
628
|
+
solveTimeMs: speedResult.solveTimeMs,
|
|
629
|
+
reason: speedResult.reason,
|
|
630
|
+
},
|
|
631
|
+
reasoning: {
|
|
632
|
+
passed: reasoningPassed,
|
|
633
|
+
score: reasoningResult.valid ? `${reasoningResult.correctCount}/${reasoningResult.totalCount}` : undefined,
|
|
634
|
+
solveTimeMs: reasoningResult.solveTimeMs,
|
|
635
|
+
reason: reasoningResult.reason,
|
|
636
|
+
},
|
|
637
|
+
totalTimeMs,
|
|
638
|
+
};
|
|
639
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ declare const app: Hono<{
|
|
|
27
27
|
Variables: Variables;
|
|
28
28
|
}, import("hono/types").BlankSchema, "/">;
|
|
29
29
|
export default app;
|
|
30
|
-
export { generateSpeedChallenge, verifySpeedChallenge, generateStandardChallenge, verifyStandardChallenge, solveSpeedChallenge, } from './challenges';
|
|
30
|
+
export { generateSpeedChallenge, verifySpeedChallenge, generateStandardChallenge, verifyStandardChallenge, generateReasoningChallenge, verifyReasoningChallenge, generateHybridChallenge, verifyHybridChallenge, solveSpeedChallenge, } from './challenges';
|
|
31
31
|
export { generateToken, verifyToken } from './auth';
|
|
32
32
|
export { checkRateLimit } from './rate-limit';
|
|
33
33
|
export { generateBadge, verifyBadge, createBadgeResponse, generateBadgeSvg, generateBadgeHtml, generateShareText, type BadgeMethod, type BadgePayload, type Badge, type ShareFormats, } from './badge';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,EAYL,KAAK,WAAW,EACjB,MAAM,cAAc,CAAC;AAOtB,KAAK,QAAQ,GAAG;IACd,UAAU,EAAE,WAAW,CAAC;IACxB,WAAW,EAAE,WAAW,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,KAAK,SAAS,GAAG;IACf,YAAY,CAAC,EAAE;QACb,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,iBAAiB,CAAC;QACxB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;CACH,CAAC;AAEF,QAAA,MAAM,GAAG;cAAwB,QAAQ;eAAa,SAAS;yCAAK,CAAC;AAuvBrE,eAAe,GAAG,CAAC;AAGnB,OAAO,EACL,sBAAsB,EACtB,oBAAoB,EACpB,yBAAyB,EACzB,uBAAuB,EACvB,0BAA0B,EAC1B,wBAAwB,EACxB,uBAAuB,EACvB,qBAAqB,EACrB,mBAAmB,GACpB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EACL,aAAa,EACb,WAAW,EACX,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,KAAK,EACV,KAAK,YAAY,GAClB,MAAM,SAAS,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -7,19 +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 } from './badge';
|
|
13
|
+
import { verifyBadge, generateBadgeSvg, generateBadgeHtml, createBadgeResponse } from './badge';
|
|
14
|
+
import streamRoutes from './routes/stream';
|
|
14
15
|
const app = new Hono();
|
|
15
16
|
// ============ MIDDLEWARE ============
|
|
16
17
|
app.use('*', cors());
|
|
18
|
+
// ============ MOUNT ROUTES ============
|
|
19
|
+
app.route('/', streamRoutes);
|
|
17
20
|
// BOTCHA discovery headers
|
|
18
21
|
app.use('*', async (c, next) => {
|
|
19
22
|
await next();
|
|
20
23
|
c.header('X-Botcha-Version', c.env.BOTCHA_VERSION || '0.2.0');
|
|
21
24
|
c.header('X-Botcha-Enabled', 'true');
|
|
22
|
-
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');
|
|
23
26
|
c.header('X-Botcha-Docs', 'https://botcha.ai/openapi.json');
|
|
24
27
|
c.header('X-Botcha-Runtime', 'cloudflare-workers');
|
|
25
28
|
});
|
|
@@ -73,14 +76,21 @@ app.get('/', (c) => {
|
|
|
73
76
|
endpoints: {
|
|
74
77
|
'/': 'API info',
|
|
75
78
|
'/health': 'Health check',
|
|
76
|
-
'/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)',
|
|
77
84
|
'/v1/token': 'Get challenge for JWT token flow (GET)',
|
|
78
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)',
|
|
79
88
|
'/agent-only': 'Protected endpoint (requires JWT)',
|
|
80
89
|
'/badge/:id': 'Badge verification page (HTML)',
|
|
81
90
|
'/badge/:id/image': 'Badge image (SVG)',
|
|
82
91
|
'/api/badge/:id': 'Badge verification (JSON)',
|
|
83
92
|
},
|
|
93
|
+
defaultChallenge: 'hybrid',
|
|
84
94
|
rateLimit: {
|
|
85
95
|
free: '100 challenges/hour/IP',
|
|
86
96
|
headers: ['X-RateLimit-Limit', 'X-RateLimit-Remaining', 'X-RateLimit-Reset'],
|
|
@@ -102,11 +112,34 @@ app.get('/health', (c) => {
|
|
|
102
112
|
return c.json({ status: 'ok', runtime: 'cloudflare-workers' });
|
|
103
113
|
});
|
|
104
114
|
// ============ V1 API ============
|
|
105
|
-
// Generate challenge (
|
|
115
|
+
// Generate challenge (hybrid by default, also supports speed and standard)
|
|
106
116
|
app.get('/v1/challenges', rateLimitMiddleware, async (c) => {
|
|
107
|
-
const type = c.req.query('type') || '
|
|
117
|
+
const type = c.req.query('type') || 'hybrid';
|
|
108
118
|
const difficulty = c.req.query('difficulty') || 'medium';
|
|
109
|
-
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') {
|
|
110
143
|
const challenge = await generateSpeedChallenge(c.env.CHALLENGES);
|
|
111
144
|
return c.json({
|
|
112
145
|
success: true,
|
|
@@ -134,11 +167,43 @@ app.get('/v1/challenges', rateLimitMiddleware, async (c) => {
|
|
|
134
167
|
});
|
|
135
168
|
}
|
|
136
169
|
});
|
|
137
|
-
// Verify challenge (
|
|
170
|
+
// Verify challenge (supports hybrid, speed, and standard)
|
|
138
171
|
app.post('/v1/challenges/:id/verify', async (c) => {
|
|
139
172
|
const id = c.req.param('id');
|
|
140
173
|
const body = await c.req.json();
|
|
141
|
-
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
|
|
142
207
|
if (type === 'speed' || answers) {
|
|
143
208
|
if (!answers || !Array.isArray(answers)) {
|
|
144
209
|
return c.json({ success: false, error: 'Missing answers array for speed challenge' }, 400);
|
|
@@ -152,17 +217,16 @@ app.post('/v1/challenges/:id/verify', async (c) => {
|
|
|
152
217
|
solveTimeMs: result.solveTimeMs,
|
|
153
218
|
});
|
|
154
219
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
const result = await verifyStandardChallenge(id, answer, c.env.CHALLENGES);
|
|
160
|
-
return c.json({
|
|
161
|
-
success: result.valid,
|
|
162
|
-
message: result.valid ? 'Challenge passed!' : result.reason,
|
|
163
|
-
solveTimeMs: result.solveTimeMs,
|
|
164
|
-
});
|
|
220
|
+
// Standard challenge
|
|
221
|
+
if (!answer) {
|
|
222
|
+
return c.json({ success: false, error: 'Missing answer for standard challenge' }, 400);
|
|
165
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
|
+
});
|
|
166
230
|
});
|
|
167
231
|
// Get challenge for token flow (includes empty token field)
|
|
168
232
|
app.get('/v1/token', rateLimitMiddleware, async (c) => {
|
|
@@ -211,6 +275,196 @@ app.post('/v1/token/verify', async (c) => {
|
|
|
211
275
|
},
|
|
212
276
|
});
|
|
213
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
|
+
});
|
|
214
468
|
// ============ PROTECTED ENDPOINT ============
|
|
215
469
|
app.get('/agent-only', requireJWT, async (c) => {
|
|
216
470
|
const payload = c.get('tokenPayload');
|
|
@@ -415,7 +669,7 @@ app.post('/api/verify-landing', async (c) => {
|
|
|
415
669
|
// ============ EXPORT ============
|
|
416
670
|
export default app;
|
|
417
671
|
// Also export utilities for use as a library
|
|
418
|
-
export { generateSpeedChallenge, verifySpeedChallenge, generateStandardChallenge, verifyStandardChallenge, solveSpeedChallenge, } from './challenges';
|
|
672
|
+
export { generateSpeedChallenge, verifySpeedChallenge, generateStandardChallenge, verifyStandardChallenge, generateReasoningChallenge, verifyReasoningChallenge, generateHybridChallenge, verifyHybridChallenge, solveSpeedChallenge, } from './challenges';
|
|
419
673
|
export { generateToken, verifyToken } from './auth';
|
|
420
674
|
export { checkRateLimit } from './rate-limit';
|
|
421
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"}
|
|
@@ -0,0 +1,242 @@
|
|
|
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 { stream as honoStream } from 'hono/streaming';
|
|
8
|
+
import { generateToken } from '../auth';
|
|
9
|
+
import { sha256First } from '../crypto';
|
|
10
|
+
const app = new Hono();
|
|
11
|
+
// ============ HELPER FUNCTIONS ============
|
|
12
|
+
/**
|
|
13
|
+
* Format SSE event
|
|
14
|
+
*/
|
|
15
|
+
function formatSSE(event, data) {
|
|
16
|
+
return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Store session in KV
|
|
20
|
+
*/
|
|
21
|
+
async function storeSession(kv, session) {
|
|
22
|
+
const ttlSeconds = Math.ceil((session.expiresAt - Date.now()) / 1000);
|
|
23
|
+
await kv.put(`stream:${session.id}`, JSON.stringify(session), { expirationTtl: Math.max(ttlSeconds, 60) });
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Get session from KV
|
|
27
|
+
*/
|
|
28
|
+
async function getSession(kv, id) {
|
|
29
|
+
const data = await kv.get(`stream:${id}`);
|
|
30
|
+
return data ? JSON.parse(data) : null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Delete session from KV
|
|
34
|
+
*/
|
|
35
|
+
async function deleteSession(kv, id) {
|
|
36
|
+
await kv.delete(`stream:${id}`);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Generate unique session ID
|
|
40
|
+
*/
|
|
41
|
+
function generateSessionId() {
|
|
42
|
+
return `sess_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
43
|
+
}
|
|
44
|
+
// ============ ROUTES ============
|
|
45
|
+
/**
|
|
46
|
+
* GET /v1/challenge/stream
|
|
47
|
+
*
|
|
48
|
+
* Opens an SSE connection and sends challenge instructions in sequence
|
|
49
|
+
*/
|
|
50
|
+
app.get('/v1/challenge/stream', async (c) => {
|
|
51
|
+
const sessionId = generateSessionId();
|
|
52
|
+
const version = c.env.BOTCHA_VERSION || '0.3.0';
|
|
53
|
+
// Create session
|
|
54
|
+
const session = {
|
|
55
|
+
id: sessionId,
|
|
56
|
+
status: 'waiting',
|
|
57
|
+
createdAt: Date.now(),
|
|
58
|
+
expiresAt: Date.now() + 5 * 60 * 1000, // 5 minutes
|
|
59
|
+
};
|
|
60
|
+
await storeSession(c.env.CHALLENGES, session);
|
|
61
|
+
return honoStream(c, async (stream) => {
|
|
62
|
+
// Send welcome event
|
|
63
|
+
await stream.write(formatSSE('welcome', {
|
|
64
|
+
session: sessionId,
|
|
65
|
+
version,
|
|
66
|
+
}));
|
|
67
|
+
// Send instructions in sequence
|
|
68
|
+
await stream.write(formatSSE('instructions', {
|
|
69
|
+
message: 'I will test if you\'re an AI agent.',
|
|
70
|
+
}));
|
|
71
|
+
await stream.sleep(100);
|
|
72
|
+
await stream.write(formatSSE('instructions', {
|
|
73
|
+
message: 'When you send "GO", I\'ll start a speed challenge.',
|
|
74
|
+
}));
|
|
75
|
+
await stream.sleep(100);
|
|
76
|
+
await stream.write(formatSSE('instructions', {
|
|
77
|
+
message: 'You must solve 5 SHA256 problems in under 500ms.',
|
|
78
|
+
}));
|
|
79
|
+
await stream.sleep(100);
|
|
80
|
+
await stream.write(formatSSE('instructions', {
|
|
81
|
+
message: 'Only real AI agents can pass. Humans are too slow.',
|
|
82
|
+
}));
|
|
83
|
+
await stream.sleep(100);
|
|
84
|
+
// Send ready event
|
|
85
|
+
await stream.write(formatSSE('ready', {
|
|
86
|
+
message: 'Send GO when ready',
|
|
87
|
+
endpoint: `/v1/challenge/stream/${sessionId}`,
|
|
88
|
+
}));
|
|
89
|
+
// Update session status
|
|
90
|
+
session.status = 'ready';
|
|
91
|
+
await storeSession(c.env.CHALLENGES, session);
|
|
92
|
+
// Keep connection alive (SSE standard practice)
|
|
93
|
+
// In production, this would be handled by connection timeout
|
|
94
|
+
// For now, we'll keep it open for 5 minutes
|
|
95
|
+
const keepAliveInterval = 15000; // 15 seconds
|
|
96
|
+
const maxWait = 5 * 60 * 1000; // 5 minutes
|
|
97
|
+
const startTime = Date.now();
|
|
98
|
+
while (Date.now() - startTime < maxWait) {
|
|
99
|
+
await stream.sleep(keepAliveInterval);
|
|
100
|
+
// Send heartbeat comment (SSE standard)
|
|
101
|
+
await stream.write(': heartbeat\n\n');
|
|
102
|
+
// Check if session was updated (challenge started)
|
|
103
|
+
const updatedSession = await getSession(c.env.CHALLENGES, sessionId);
|
|
104
|
+
if (!updatedSession || updatedSession.status !== 'ready') {
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
/**
|
|
111
|
+
* POST /v1/challenge/stream/:session
|
|
112
|
+
*
|
|
113
|
+
* Handle actions: "go" (start challenge) or "solve" (submit answers)
|
|
114
|
+
*/
|
|
115
|
+
app.post('/v1/challenge/stream/:session', async (c) => {
|
|
116
|
+
const sessionId = c.req.param('session');
|
|
117
|
+
const body = await c.req.json();
|
|
118
|
+
const { action, answers } = body;
|
|
119
|
+
// Validate session
|
|
120
|
+
const session = await getSession(c.env.CHALLENGES, sessionId);
|
|
121
|
+
if (!session) {
|
|
122
|
+
return c.json({
|
|
123
|
+
success: false,
|
|
124
|
+
error: 'SESSION_NOT_FOUND',
|
|
125
|
+
message: 'Session not found or expired',
|
|
126
|
+
}, 404);
|
|
127
|
+
}
|
|
128
|
+
// Handle "go" action - start challenge
|
|
129
|
+
if (action === 'go') {
|
|
130
|
+
if (session.status !== 'ready') {
|
|
131
|
+
return c.json({
|
|
132
|
+
success: false,
|
|
133
|
+
error: 'INVALID_STATE',
|
|
134
|
+
message: `Session is in ${session.status} state, expected ready`,
|
|
135
|
+
}, 400);
|
|
136
|
+
}
|
|
137
|
+
// Generate challenge problems
|
|
138
|
+
const problems = [];
|
|
139
|
+
const expectedAnswers = [];
|
|
140
|
+
for (let i = 0; i < 5; i++) {
|
|
141
|
+
const num = Math.floor(Math.random() * 900000) + 100000;
|
|
142
|
+
problems.push({ num, operation: 'sha256_first8' });
|
|
143
|
+
expectedAnswers.push(await sha256First(num.toString(), 8));
|
|
144
|
+
}
|
|
145
|
+
// Update session
|
|
146
|
+
session.status = 'challenged';
|
|
147
|
+
session.problems = problems;
|
|
148
|
+
session.expectedAnswers = expectedAnswers;
|
|
149
|
+
session.timerStart = Date.now();
|
|
150
|
+
await storeSession(c.env.CHALLENGES, session);
|
|
151
|
+
// Return challenge event
|
|
152
|
+
return c.json({
|
|
153
|
+
success: true,
|
|
154
|
+
event: 'challenge',
|
|
155
|
+
data: {
|
|
156
|
+
problems,
|
|
157
|
+
timeLimit: 500,
|
|
158
|
+
instructions: 'Compute SHA256 of each number, return first 8 hex chars',
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
// Handle "solve" action - verify answers
|
|
163
|
+
if (action === 'solve') {
|
|
164
|
+
if (session.status !== 'challenged') {
|
|
165
|
+
return c.json({
|
|
166
|
+
success: false,
|
|
167
|
+
error: 'INVALID_STATE',
|
|
168
|
+
message: `Session is in ${session.status} state, expected challenged`,
|
|
169
|
+
}, 400);
|
|
170
|
+
}
|
|
171
|
+
if (!answers || !Array.isArray(answers)) {
|
|
172
|
+
return c.json({
|
|
173
|
+
success: false,
|
|
174
|
+
error: 'MISSING_ANSWERS',
|
|
175
|
+
message: 'Missing answers array',
|
|
176
|
+
}, 400);
|
|
177
|
+
}
|
|
178
|
+
const now = Date.now();
|
|
179
|
+
const solveTimeMs = now - (session.timerStart || now);
|
|
180
|
+
// Delete session to prevent replay
|
|
181
|
+
await deleteSession(c.env.CHALLENGES, sessionId);
|
|
182
|
+
// Check timing
|
|
183
|
+
if (solveTimeMs > 500) {
|
|
184
|
+
return c.json({
|
|
185
|
+
success: false,
|
|
186
|
+
event: 'result',
|
|
187
|
+
data: {
|
|
188
|
+
success: false,
|
|
189
|
+
verdict: '🚫 TOO SLOW',
|
|
190
|
+
message: `Took ${solveTimeMs}ms, limit was 500ms`,
|
|
191
|
+
solveTimeMs,
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
// Check answers
|
|
196
|
+
if (answers.length !== 5) {
|
|
197
|
+
return c.json({
|
|
198
|
+
success: false,
|
|
199
|
+
event: 'result',
|
|
200
|
+
data: {
|
|
201
|
+
success: false,
|
|
202
|
+
verdict: '❌ WRONG FORMAT',
|
|
203
|
+
message: 'Must provide exactly 5 answers',
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
for (let i = 0; i < 5; i++) {
|
|
208
|
+
if (answers[i]?.toLowerCase() !== session.expectedAnswers[i]) {
|
|
209
|
+
return c.json({
|
|
210
|
+
success: false,
|
|
211
|
+
event: 'result',
|
|
212
|
+
data: {
|
|
213
|
+
success: false,
|
|
214
|
+
verdict: '❌ WRONG ANSWER',
|
|
215
|
+
message: `Wrong answer for problem ${i + 1}`,
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Success! Generate JWT token
|
|
221
|
+
const token = await generateToken(sessionId, solveTimeMs, c.env.JWT_SECRET);
|
|
222
|
+
return c.json({
|
|
223
|
+
success: true,
|
|
224
|
+
event: 'result',
|
|
225
|
+
data: {
|
|
226
|
+
success: true,
|
|
227
|
+
verdict: '🤖 VERIFIED',
|
|
228
|
+
message: `Challenge passed in ${solveTimeMs}ms! You are a bot.`,
|
|
229
|
+
solveTimeMs,
|
|
230
|
+
token,
|
|
231
|
+
expiresIn: '1h',
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
// Unknown action
|
|
236
|
+
return c.json({
|
|
237
|
+
success: false,
|
|
238
|
+
error: 'UNKNOWN_ACTION',
|
|
239
|
+
message: `Unknown action: ${action}. Expected "go" or "solve"`,
|
|
240
|
+
}, 400);
|
|
241
|
+
});
|
|
242
|
+
export default app;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dupecom/botcha-cloudflare",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "BOTCHA for Cloudflare Workers - Prove you're a bot. Humans need not apply.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"README.md"
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
|
-
"dev": "
|
|
19
|
+
"dev": "bun run scripts/dev-intro.ts",
|
|
20
20
|
"deploy": "wrangler deploy",
|
|
21
21
|
"build": "tsc",
|
|
22
22
|
"prepublishOnly": "bun run build",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@cloudflare/workers-types": "^4.20250124.0",
|
|
48
48
|
"typescript": "^5.9.3",
|
|
49
|
-
"wrangler": "^4.
|
|
49
|
+
"wrangler": "^4.63.0",
|
|
50
50
|
"vitest": "^3.0.0"
|
|
51
51
|
}
|
|
52
52
|
}
|