@dupecom/botcha-cloudflare 0.3.3 → 0.10.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.
Files changed (47) hide show
  1. package/dist/analytics.d.ts +60 -0
  2. package/dist/analytics.d.ts.map +1 -0
  3. package/dist/analytics.js +130 -0
  4. package/dist/apps.d.ts +159 -0
  5. package/dist/apps.d.ts.map +1 -0
  6. package/dist/apps.js +307 -0
  7. package/dist/auth.d.ts +93 -6
  8. package/dist/auth.d.ts.map +1 -1
  9. package/dist/auth.js +251 -9
  10. package/dist/challenges.d.ts +31 -7
  11. package/dist/challenges.d.ts.map +1 -1
  12. package/dist/challenges.js +551 -144
  13. package/dist/dashboard/api.d.ts +70 -0
  14. package/dist/dashboard/api.d.ts.map +1 -0
  15. package/dist/dashboard/api.js +546 -0
  16. package/dist/dashboard/auth.d.ts +183 -0
  17. package/dist/dashboard/auth.d.ts.map +1 -0
  18. package/dist/dashboard/auth.js +401 -0
  19. package/dist/dashboard/device-code.d.ts +43 -0
  20. package/dist/dashboard/device-code.d.ts.map +1 -0
  21. package/dist/dashboard/device-code.js +77 -0
  22. package/dist/dashboard/index.d.ts +31 -0
  23. package/dist/dashboard/index.d.ts.map +1 -0
  24. package/dist/dashboard/index.js +64 -0
  25. package/dist/dashboard/layout.d.ts +47 -0
  26. package/dist/dashboard/layout.d.ts.map +1 -0
  27. package/dist/dashboard/layout.js +38 -0
  28. package/dist/dashboard/pages.d.ts +11 -0
  29. package/dist/dashboard/pages.d.ts.map +1 -0
  30. package/dist/dashboard/pages.js +18 -0
  31. package/dist/dashboard/styles.d.ts +11 -0
  32. package/dist/dashboard/styles.d.ts.map +1 -0
  33. package/dist/dashboard/styles.js +633 -0
  34. package/dist/email.d.ts +44 -0
  35. package/dist/email.d.ts.map +1 -0
  36. package/dist/email.js +119 -0
  37. package/dist/index.d.ts +3 -0
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +644 -50
  40. package/dist/rate-limit.d.ts +11 -1
  41. package/dist/rate-limit.d.ts.map +1 -1
  42. package/dist/rate-limit.js +13 -2
  43. package/dist/routes/stream.js +1 -1
  44. package/dist/static.d.ts +728 -0
  45. package/dist/static.d.ts.map +1 -0
  46. package/dist/static.js +818 -0
  47. package/package.json +1 -1
@@ -69,10 +69,10 @@ async function deleteChallenge(kv, id) {
69
69
  }
70
70
  // ============ SPEED CHALLENGE ============
71
71
  /**
72
- * Generate a speed challenge: 5 SHA256 problems, 500ms to solve ALL
72
+ * Generate a speed challenge: 5 SHA256 problems, RTT-aware timeout
73
73
  * Trivial for AI, impossible for humans to copy-paste fast enough
74
74
  */
75
- export async function generateSpeedChallenge(kv) {
75
+ export async function generateSpeedChallenge(kv, clientTimestamp, app_id) {
76
76
  cleanExpired();
77
77
  const id = uuid();
78
78
  const problems = [];
@@ -82,25 +82,58 @@ export async function generateSpeedChallenge(kv) {
82
82
  problems.push({ num, operation: 'sha256_first8' });
83
83
  expectedAnswers.push(await sha256First(num.toString(), 8));
84
84
  }
85
- const timeLimit = 500;
85
+ // RTT-aware timeout calculation
86
+ const baseTimeLimit = 500; // Base computation time for AI agents
87
+ const MAX_RTT_MS = 5000; // Cap RTT to prevent timestamp spoofing (5s max)
88
+ const MAX_TIMESTAMP_AGE_MS = 30000; // Reject timestamps older than 30s
89
+ const now = Date.now();
90
+ let rttMs = 0;
91
+ let adjustedTimeLimit = baseTimeLimit;
92
+ let rttInfo = undefined;
93
+ if (clientTimestamp && clientTimestamp > 0) {
94
+ // Reject timestamps in the future or too far in the past (anti-spoofing)
95
+ const age = now - clientTimestamp;
96
+ if (age >= 0 && age <= MAX_TIMESTAMP_AGE_MS) {
97
+ // Calculate RTT from client timestamp, capped to prevent abuse
98
+ rttMs = Math.min(age, MAX_RTT_MS);
99
+ // Adjust timeout: base + (2 * RTT) + 100ms buffer
100
+ // The 2x RTT accounts for request + response network time
101
+ adjustedTimeLimit = Math.max(baseTimeLimit, baseTimeLimit + (2 * rttMs) + 100);
102
+ rttInfo = {
103
+ measuredRtt: rttMs,
104
+ adjustedTimeout: adjustedTimeLimit,
105
+ explanation: `RTT: ${rttMs}ms → Timeout: ${baseTimeLimit}ms + (2×${rttMs}ms) + 100ms = ${adjustedTimeLimit}ms`,
106
+ };
107
+ }
108
+ // else: invalid timestamp silently ignored, use base timeout
109
+ }
86
110
  const challenge = {
87
111
  id,
88
112
  problems,
89
113
  expectedAnswers,
90
- issuedAt: Date.now(),
91
- expiresAt: Date.now() + timeLimit + 100, // tiny grace
114
+ issuedAt: now,
115
+ expiresAt: now + adjustedTimeLimit + 50, // Small server-side grace period
116
+ baseTimeLimit,
117
+ adjustedTimeLimit,
118
+ rttMs,
119
+ app_id,
92
120
  };
93
121
  // Store in KV with 5 minute TTL (safety buffer for time checks)
94
122
  await storeChallenge(kv, id, challenge, 300);
123
+ const pipelineHint = ' Tip: compute all hashes and submit in a single HTTP request. Sequential shell commands will likely exceed the time limit.';
124
+ const instructions = rttMs > 0
125
+ ? `Compute SHA256 of each number, return first 8 hex chars of each. Submit as array. You have ${adjustedTimeLimit}ms (adjusted for your ${rttMs}ms network latency).${pipelineHint}`
126
+ : `Compute SHA256 of each number, return first 8 hex chars of each. Submit as array. You have 500ms.${pipelineHint}`;
95
127
  return {
96
128
  id,
97
129
  problems,
98
- timeLimit,
99
- instructions: 'Compute SHA256 of each number, return first 8 hex chars of each. Submit as array. You have 500ms.',
130
+ timeLimit: adjustedTimeLimit,
131
+ instructions,
132
+ rttInfo,
100
133
  };
101
134
  }
102
135
  /**
103
- * Verify a speed challenge response
136
+ * Verify a speed challenge response with RTT-aware timeout
104
137
  */
105
138
  export async function verifySpeedChallenge(id, answers, kv) {
106
139
  const challenge = await getChallenge(kv, id, true);
@@ -111,8 +144,21 @@ export async function verifySpeedChallenge(id, answers, kv) {
111
144
  const solveTimeMs = now - challenge.issuedAt;
112
145
  // Delete challenge immediately to prevent replay attacks
113
146
  await deleteChallenge(kv, id);
147
+ // Use the challenge's adjusted timeout, fallback to base if not available
148
+ const timeLimit = challenge.adjustedTimeLimit || challenge.baseTimeLimit || 500;
114
149
  if (now > challenge.expiresAt) {
115
- return { valid: false, reason: `Too slow! Took ${solveTimeMs}ms, limit was 500ms` };
150
+ const rttExplanation = challenge.rttMs
151
+ ? ` (RTT-adjusted: ${challenge.rttMs}ms network + ${challenge.baseTimeLimit}ms compute = ${timeLimit}ms limit)`
152
+ : '';
153
+ return {
154
+ valid: false,
155
+ reason: `Too slow! Took ${solveTimeMs}ms, limit was ${timeLimit}ms${rttExplanation}`,
156
+ rttInfo: challenge.rttMs ? {
157
+ measuredRtt: challenge.rttMs,
158
+ adjustedTimeout: timeLimit,
159
+ actualTime: solveTimeMs,
160
+ } : undefined,
161
+ };
116
162
  }
117
163
  if (!Array.isArray(answers) || answers.length !== 5) {
118
164
  return { valid: false, reason: 'Must provide exactly 5 answers as array' };
@@ -122,7 +168,16 @@ export async function verifySpeedChallenge(id, answers, kv) {
122
168
  return { valid: false, reason: `Wrong answer for challenge ${i + 1}` };
123
169
  }
124
170
  }
125
- return { valid: true, solveTimeMs };
171
+ return {
172
+ valid: true,
173
+ solveTimeMs,
174
+ app_id: challenge.app_id,
175
+ rttInfo: challenge.rttMs ? {
176
+ measuredRtt: challenge.rttMs,
177
+ adjustedTimeout: timeLimit,
178
+ actualTime: solveTimeMs,
179
+ } : undefined,
180
+ };
126
181
  }
127
182
  // ============ STANDARD CHALLENGE ============
128
183
  const DIFFICULTY_CONFIG = {
@@ -133,28 +188,31 @@ const DIFFICULTY_CONFIG = {
133
188
  /**
134
189
  * Generate a standard challenge: compute SHA256 of concatenated primes
135
190
  */
136
- export async function generateStandardChallenge(difficulty = 'medium', kv) {
191
+ export async function generateStandardChallenge(difficulty = 'medium', kv, app_id) {
137
192
  cleanExpired();
138
193
  const id = uuid();
139
194
  const config = DIFFICULTY_CONFIG[difficulty];
195
+ // Random salt makes each challenge unique — precomputed lookup tables won't work
196
+ const salt = uuid().replace(/-/g, '').substring(0, 16);
140
197
  const primes = generatePrimes(config.primes);
141
- const concatenated = primes.join('');
198
+ const concatenated = primes.join('') + salt;
142
199
  const hash = await sha256(concatenated);
143
200
  const answer = hash.substring(0, 16);
144
201
  const challenge = {
145
202
  id,
146
- puzzle: `Compute SHA256 of the first ${config.primes} prime numbers concatenated (no separators). Return the first 16 hex characters.`,
203
+ puzzle: `Compute SHA256 of the first ${config.primes} prime numbers concatenated (no separators) followed by the salt "${salt}". Return the first 16 hex characters.`,
147
204
  expectedAnswer: answer,
148
205
  expiresAt: Date.now() + config.timeLimit + 1000,
149
206
  difficulty,
207
+ app_id,
150
208
  };
151
209
  // Store in KV with 5 minute TTL
152
210
  await storeChallenge(kv, id, challenge, 300);
153
211
  return {
154
212
  id,
155
- puzzle: `Compute SHA256 of the first ${config.primes} prime numbers concatenated (no separators). Return the first 16 hex characters.`,
213
+ puzzle: `Compute SHA256 of the first ${config.primes} prime numbers concatenated (no separators) followed by the salt "${salt}". Return the first 16 hex characters.`,
156
214
  timeLimit: config.timeLimit,
157
- hint: `Example: First 5 primes = "235711" → SHA256 → first 16 chars`,
215
+ hint: `Example: First 5 primes + salt = "235711${salt}" → SHA256 → first 16 chars`,
158
216
  };
159
217
  }
160
218
  /**
@@ -186,20 +244,22 @@ const landingTokens = new Map();
186
244
  */
187
245
  export async function verifyLandingChallenge(answer, timestamp, kv) {
188
246
  cleanExpired();
189
- // Verify timestamp is recent (within 5 minutes)
247
+ // Verify timestamp is recent (within 2 minutes — tighter window for security)
190
248
  const submittedTime = new Date(timestamp).getTime();
191
249
  const now = Date.now();
192
- if (Math.abs(now - submittedTime) > 5 * 60 * 1000) {
193
- return { valid: false, error: 'Timestamp too old or in future' };
250
+ if (Number.isNaN(submittedTime) || Math.abs(now - submittedTime) > 2 * 60 * 1000) {
251
+ return { valid: false, error: 'Timestamp expired or invalid. Request a fresh challenge.' };
194
252
  }
195
- // Calculate expected answer for today
253
+ // Per-request nonce: include the timestamp in the hash input so answers are unique per request
254
+ // This prevents answer sharing — each timestamp produces a different expected answer
196
255
  const today = new Date().toISOString().split('T')[0];
197
- const expectedHash = (await sha256(`BOTCHA-LANDING-${today}`)).substring(0, 16);
256
+ const expectedHash = (await sha256(`BOTCHA-LANDING-${today}-${timestamp}`)).substring(0, 16);
198
257
  if (answer.toLowerCase() !== expectedHash.toLowerCase()) {
199
258
  return {
200
259
  valid: false,
201
260
  error: 'Incorrect answer',
202
- hint: `Expected SHA256('BOTCHA-LANDING-${today}') first 16 chars`
261
+ // Don't leak the formula — only give a generic hint
262
+ hint: 'Parse the challenge from <script type="application/botcha+json"> on the landing page and compute the answer.',
203
263
  };
204
264
  }
205
265
  // Generate token
@@ -249,149 +309,490 @@ export async function solveSpeedChallenge(problems) {
249
309
  // ============ REASONING CHALLENGE ============
250
310
  // In-memory storage for reasoning challenges
251
311
  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',
312
+ // ============ PARAMETERIZED QUESTION GENERATORS ============
313
+ // These generate unique questions each time, so a static lookup table won't work.
314
+ function randInt(min, max) {
315
+ return Math.floor(Math.random() * (max - min + 1)) + min;
316
+ }
317
+ function pickRandom(arr) {
318
+ return arr[Math.floor(Math.random() * arr.length)];
319
+ }
320
+ // --- Math generators (randomized numbers each time) ---
321
+ function genMathAdd() {
322
+ const a = randInt(100, 999);
323
+ const b = randInt(100, 999);
324
+ return {
325
+ id: `math-add-${uuid().substring(0, 8)}`,
326
+ question: `What is ${a} + ${b}?`,
327
+ category: 'math',
328
+ acceptedAnswers: [(a + b).toString()],
329
+ };
330
+ }
331
+ function genMathMultiply() {
332
+ const a = randInt(12, 99);
333
+ const b = randInt(12, 99);
334
+ return {
335
+ id: `math-mul-${uuid().substring(0, 8)}`,
336
+ question: `What is ${a} × ${b}?`,
337
+ category: 'math',
338
+ acceptedAnswers: [(a * b).toString()],
339
+ };
340
+ }
341
+ function genMathModulo() {
342
+ const a = randInt(50, 999);
343
+ const b = randInt(3, 19);
344
+ return {
345
+ id: `math-mod-${uuid().substring(0, 8)}`,
346
+ question: `What is ${a} % ${b} (modulo)?`,
347
+ category: 'math',
348
+ acceptedAnswers: [(a % b).toString()],
349
+ };
350
+ }
351
+ function genMathSheep() {
352
+ const total = randInt(15, 50);
353
+ const remaining = randInt(3, total - 2);
354
+ return {
355
+ id: `math-sheep-${uuid().substring(0, 8)}`,
356
+ question: `A farmer has ${total} sheep. All but ${remaining} run away. How many sheep does he have left? Answer with just the number.`,
357
+ category: 'math',
358
+ acceptedAnswers: [remaining.toString()],
359
+ };
360
+ }
361
+ function genMathDoubling() {
362
+ const days = randInt(20, 60);
363
+ return {
364
+ id: `math-double-${uuid().substring(0, 8)}`,
365
+ question: `A patch of lily pads doubles in size every day. If it takes ${days} days to cover the entire lake, how many days to cover half? Answer with just the number.`,
366
+ category: 'math',
367
+ acceptedAnswers: [(days - 1).toString()],
368
+ };
369
+ }
370
+ function genMathMachines() {
371
+ const n = pickRandom([5, 7, 8, 10, 12]);
372
+ const m = randInt(50, 200);
373
+ return {
374
+ id: `math-machines-${uuid().substring(0, 8)}`,
375
+ question: `If it takes ${n} machines ${n} minutes to make ${n} widgets, how many minutes would it take ${m} machines to make ${m} widgets? Answer with just the number.`,
376
+ category: 'math',
377
+ acceptedAnswers: [n.toString()],
378
+ };
379
+ }
380
+ // --- Code generators (randomized values) ---
381
+ function genCodeModulo() {
382
+ const a = randInt(20, 200);
383
+ const b = randInt(3, 15);
384
+ return {
385
+ id: `code-mod-${uuid().substring(0, 8)}`,
386
+ question: `In most programming languages, what does ${a} % ${b} evaluate to?`,
387
+ category: 'code',
388
+ acceptedAnswers: [(a % b).toString()],
389
+ };
390
+ }
391
+ function genCodeBitwise() {
392
+ const a = randInt(1, 31);
393
+ const b = randInt(1, 31);
394
+ const op = pickRandom(['&', '|', '^']);
395
+ const opName = op === '&' ? 'AND' : op === '|' ? 'OR' : 'XOR';
396
+ let answer;
397
+ if (op === '&')
398
+ answer = a & b;
399
+ else if (op === '|')
400
+ answer = a | b;
401
+ else
402
+ answer = a ^ b;
403
+ return {
404
+ id: `code-bit-${uuid().substring(0, 8)}`,
405
+ question: `What is ${a} ${op} ${b} (bitwise ${opName})? Answer with just the number.`,
406
+ category: 'code',
407
+ acceptedAnswers: [answer.toString()],
408
+ };
409
+ }
410
+ function genCodeStringLen() {
411
+ // Generate random alphanumeric strings of varying lengths (3-20 chars)
412
+ // This creates effectively infinite answer space (18 possible lengths × countless string combinations)
413
+ const length = randInt(3, 20);
414
+ const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
415
+ let word = '';
416
+ for (let i = 0; i < length; i++) {
417
+ word += chars[Math.floor(Math.random() * chars.length)];
418
+ }
419
+ return {
420
+ id: `code-strlen-${uuid().substring(0, 8)}`,
421
+ question: `What is the length of the string "${word}"? Answer with just the number.`,
422
+ category: 'code',
423
+ acceptedAnswers: [word.length.toString()],
424
+ };
425
+ }
426
+ // --- Logic generators (randomized names/items) ---
427
+ function genLogicSyllogism() {
428
+ const groups = [
429
+ ['Bloops', 'Razzies', 'Lazzies'],
430
+ ['Florps', 'Zinkies', 'Mopples'],
431
+ ['Grunts', 'Tazzles', 'Wibbles'],
432
+ ['Plonks', 'Snazzles', 'Krinkles'],
433
+ ['Dweems', 'Fozzits', 'Glimmers'],
434
+ ];
435
+ const [a, b, c] = pickRandom(groups);
436
+ return {
437
+ id: `logic-syl-${uuid().substring(0, 8)}`,
438
+ question: `If all ${a} are ${b} and all ${b} are ${c}, are all ${a} definitely ${c}? Answer yes or no.`,
439
+ category: 'logic',
440
+ acceptedAnswers: ['yes'],
441
+ };
442
+ }
443
+ function genLogicNegation() {
444
+ const total = randInt(20, 100);
445
+ const keep = randInt(3, total - 5);
446
+ return {
447
+ id: `logic-neg-${uuid().substring(0, 8)}`,
448
+ question: `There are ${total} marbles in a bag. You remove all but ${keep}. How many marbles are left in the bag? Answer with just the number.`,
449
+ category: 'logic',
450
+ acceptedAnswers: [keep.toString()],
451
+ };
452
+ }
453
+ function genLogicSequence() {
454
+ const start = randInt(2, 20);
455
+ const step = randInt(2, 8);
456
+ const seq = [start, start + step, start + 2 * step, start + 3 * step];
457
+ return {
458
+ id: `logic-seq-${uuid().substring(0, 8)}`,
459
+ question: `What comes next in the sequence: ${seq.join(', ')}, ___? Answer with just the number.`,
460
+ category: 'logic',
461
+ acceptedAnswers: [(start + 4 * step).toString()],
462
+ };
463
+ }
464
+ // --- Wordplay / static (with randomized IDs so lookup by ID fails) ---
465
+ const WORDPLAY_GENERATORS = [
466
+ // Connection riddles (original + new)
467
+ () => ({
468
+ id: `wp-${uuid().substring(0, 8)}`,
282
469
  question: 'What single word connects: apple, Newton, gravity?',
283
470
  category: 'wordplay',
284
471
  acceptedAnswers: ['tree', 'fall', 'falling'],
285
- },
286
- {
287
- id: 'wordplay-2',
472
+ }),
473
+ () => ({
474
+ id: `wp-${uuid().substring(0, 8)}`,
288
475
  question: 'What single word connects: key, piano, computer?',
289
476
  category: 'wordplay',
290
477
  acceptedAnswers: ['keyboard', 'board', 'keys'],
291
- },
292
- {
293
- id: 'wordplay-3',
478
+ }),
479
+ () => ({
480
+ id: `wp-${uuid().substring(0, 8)}`,
294
481
  question: 'What single word connects: river, money, blood?',
295
482
  category: 'wordplay',
296
483
  acceptedAnswers: ['bank', 'flow', 'stream'],
297
- },
298
- {
299
- id: 'wordplay-4',
484
+ }),
485
+ () => ({
486
+ id: `wp-${uuid().substring(0, 8)}`,
300
487
  question: 'What word can precede: light, house, shine?',
301
488
  category: 'wordplay',
302
489
  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',
490
+ }),
491
+ () => ({
492
+ id: `wp-${uuid().substring(0, 8)}`,
493
+ question: 'What single word connects: fire, ice, boxing?',
494
+ category: 'wordplay',
495
+ acceptedAnswers: ['ring', 'fight', 'match'],
496
+ }),
497
+ () => ({
498
+ id: `wp-${uuid().substring(0, 8)}`,
499
+ question: 'What single word connects: music, radio, ocean?',
500
+ category: 'wordplay',
501
+ acceptedAnswers: ['wave', 'waves', 'frequency'],
502
+ }),
503
+ () => ({
504
+ id: `wp-${uuid().substring(0, 8)}`,
505
+ question: 'What word follows: high, middle, private?',
506
+ category: 'wordplay',
507
+ acceptedAnswers: ['school'],
508
+ }),
509
+ () => ({
510
+ id: `wp-${uuid().substring(0, 8)}`,
511
+ question: 'What word connects: sleeping, travel, time?',
512
+ category: 'wordplay',
513
+ acceptedAnswers: ['bag'],
514
+ }),
515
+ // Common sense riddles (original + new)
516
+ () => ({
517
+ id: `wp-${uuid().substring(0, 8)}`,
376
518
  question: 'What gets wetter the more it dries?',
377
519
  category: 'common-sense',
378
520
  acceptedAnswers: ['towel', 'a towel', 'cloth', 'rag'],
379
- },
380
- {
381
- id: 'sense-3',
521
+ }),
522
+ () => ({
523
+ id: `wp-${uuid().substring(0, 8)}`,
382
524
  question: 'What can you catch but not throw?',
383
525
  category: 'common-sense',
384
526
  acceptedAnswers: ['cold', 'a cold', 'breath', 'your breath', 'feelings', 'disease'],
385
- },
527
+ }),
528
+ () => ({
529
+ id: `wp-${uuid().substring(0, 8)}`,
530
+ question: 'What has keys but no locks, space but no room, and you can enter but not go inside?',
531
+ category: 'common-sense',
532
+ acceptedAnswers: ['keyboard', 'a keyboard'],
533
+ }),
534
+ () => ({
535
+ id: `wp-${uuid().substring(0, 8)}`,
536
+ question: 'What runs but never walks, has a mouth but never talks?',
537
+ category: 'common-sense',
538
+ acceptedAnswers: ['river', 'a river', 'stream'],
539
+ }),
540
+ () => ({
541
+ id: `wp-${uuid().substring(0, 8)}`,
542
+ question: 'What has hands but cannot clap?',
543
+ category: 'common-sense',
544
+ acceptedAnswers: ['clock', 'a clock', 'watch'],
545
+ }),
546
+ () => ({
547
+ id: `wp-${uuid().substring(0, 8)}`,
548
+ question: 'What has a head and tail but no body?',
549
+ category: 'common-sense',
550
+ acceptedAnswers: ['coin', 'a coin'],
551
+ }),
552
+ () => ({
553
+ id: `wp-${uuid().substring(0, 8)}`,
554
+ question: 'What goes up but never comes down?',
555
+ category: 'common-sense',
556
+ acceptedAnswers: ['age', 'your age'],
557
+ }),
558
+ () => ({
559
+ id: `wp-${uuid().substring(0, 8)}`,
560
+ question: 'What has teeth but cannot bite?',
561
+ category: 'common-sense',
562
+ acceptedAnswers: ['comb', 'a comb', 'saw', 'zipper', 'gear'],
563
+ }),
564
+ () => ({
565
+ id: `wp-${uuid().substring(0, 8)}`,
566
+ question: 'What can fill a room but takes up no space?',
567
+ category: 'common-sense',
568
+ acceptedAnswers: ['light', 'air', 'sound', 'darkness'],
569
+ }),
570
+ () => ({
571
+ id: `wp-${uuid().substring(0, 8)}`,
572
+ question: 'What has a neck but no head?',
573
+ category: 'common-sense',
574
+ acceptedAnswers: ['bottle', 'a bottle'],
575
+ }),
576
+ // Analogies (original + new)
577
+ () => ({
578
+ id: `wp-${uuid().substring(0, 8)}`,
579
+ question: 'Complete the analogy: Fish is to water as bird is to ___',
580
+ category: 'analogy',
581
+ acceptedAnswers: ['air', 'sky', 'atmosphere'],
582
+ }),
583
+ () => ({
584
+ id: `wp-${uuid().substring(0, 8)}`,
585
+ question: 'Complete the analogy: Eye is to see as ear is to ___',
586
+ category: 'analogy',
587
+ acceptedAnswers: ['hear', 'listen', 'hearing', 'listening'],
588
+ }),
589
+ () => ({
590
+ id: `wp-${uuid().substring(0, 8)}`,
591
+ question: 'Complete the analogy: Painter is to brush as writer is to ___',
592
+ category: 'analogy',
593
+ acceptedAnswers: ['pen', 'pencil', 'keyboard', 'typewriter', 'quill'],
594
+ }),
595
+ () => ({
596
+ id: `wp-${uuid().substring(0, 8)}`,
597
+ question: 'Complete the analogy: Hot is to cold as day is to ___',
598
+ category: 'analogy',
599
+ acceptedAnswers: ['night'],
600
+ }),
601
+ () => ({
602
+ id: `wp-${uuid().substring(0, 8)}`,
603
+ question: 'Complete the analogy: Doctor is to patient as teacher is to ___',
604
+ category: 'analogy',
605
+ acceptedAnswers: ['student', 'students', 'pupil'],
606
+ }),
607
+ () => ({
608
+ id: `wp-${uuid().substring(0, 8)}`,
609
+ question: 'Complete the analogy: Wheel is to car as sail is to ___',
610
+ category: 'analogy',
611
+ acceptedAnswers: ['boat', 'ship', 'sailboat'],
612
+ }),
613
+ () => ({
614
+ id: `wp-${uuid().substring(0, 8)}`,
615
+ question: 'Complete the analogy: Chef is to kitchen as scientist is to ___',
616
+ category: 'analogy',
617
+ acceptedAnswers: ['laboratory', 'lab'],
618
+ }),
619
+ () => ({
620
+ id: `wp-${uuid().substring(0, 8)}`,
621
+ question: 'Complete the analogy: Bark is to dog as meow is to ___',
622
+ category: 'analogy',
623
+ acceptedAnswers: ['cat'],
624
+ }),
625
+ // Anagrams
626
+ () => ({
627
+ id: `wp-${uuid().substring(0, 8)}`,
628
+ question: 'Rearrange the letters in "listen" to make another common word.',
629
+ category: 'wordplay',
630
+ acceptedAnswers: ['silent', 'enlist'],
631
+ }),
632
+ () => ({
633
+ id: `wp-${uuid().substring(0, 8)}`,
634
+ question: 'Rearrange the letters in "earth" to make another common word.',
635
+ category: 'wordplay',
636
+ acceptedAnswers: ['heart', 'hater'],
637
+ }),
638
+ () => ({
639
+ id: `wp-${uuid().substring(0, 8)}`,
640
+ question: 'Rearrange the letters in "stream" to make another word meaning "leader".',
641
+ category: 'wordplay',
642
+ acceptedAnswers: ['master'],
643
+ }),
644
+ () => ({
645
+ id: `wp-${uuid().substring(0, 8)}`,
646
+ question: 'Rearrange the letters in "stop" to make containers.',
647
+ category: 'wordplay',
648
+ acceptedAnswers: ['pots', 'spot', 'tops'],
649
+ }),
650
+ // Code/CS riddles
651
+ () => ({
652
+ id: `wp-${uuid().substring(0, 8)}`,
653
+ question: 'What data structure uses LIFO (Last In, First Out)?',
654
+ category: 'code',
655
+ acceptedAnswers: ['stack', 'a stack'],
656
+ }),
657
+ () => ({
658
+ id: `wp-${uuid().substring(0, 8)}`,
659
+ question: 'What data structure uses FIFO (First In, First Out)?',
660
+ category: 'code',
661
+ acceptedAnswers: ['queue', 'a queue'],
662
+ }),
663
+ () => ({
664
+ id: `wp-${uuid().substring(0, 8)}`,
665
+ question: 'In programming, what comes after "if" and "else if"?',
666
+ category: 'code',
667
+ acceptedAnswers: ['else'],
668
+ }),
669
+ () => ({
670
+ id: `wp-${uuid().substring(0, 8)}`,
671
+ question: 'What is the opposite of "true" in boolean logic?',
672
+ category: 'code',
673
+ acceptedAnswers: ['false'],
674
+ }),
675
+ () => ({
676
+ id: `wp-${uuid().substring(0, 8)}`,
677
+ question: 'What keyword is used to define a function in JavaScript?',
678
+ category: 'code',
679
+ acceptedAnswers: ['function', 'const', 'let', 'var', 'async'],
680
+ }),
681
+ // Word puzzles
682
+ () => ({
683
+ id: `wp-${uuid().substring(0, 8)}`,
684
+ question: 'What 5-letter word becomes shorter when you add two letters to it?',
685
+ category: 'wordplay',
686
+ acceptedAnswers: ['short', 'shorter'],
687
+ }),
688
+ () => ({
689
+ id: `wp-${uuid().substring(0, 8)}`,
690
+ question: 'What word starts with "e" and ends with "e" but only has one letter in it?',
691
+ category: 'wordplay',
692
+ acceptedAnswers: ['envelope'],
693
+ }),
694
+ () => ({
695
+ id: `wp-${uuid().substring(0, 8)}`,
696
+ question: 'What begins with T, ends with T, and has T in it?',
697
+ category: 'wordplay',
698
+ acceptedAnswers: ['teapot', 'a teapot'],
699
+ }),
700
+ () => ({
701
+ id: `wp-${uuid().substring(0, 8)}`,
702
+ question: 'Remove one letter from "startling" to create a new word. What is it?',
703
+ category: 'wordplay',
704
+ acceptedAnswers: ['starting', 'starling'],
705
+ }),
706
+ // Math/Logic wordplay
707
+ () => ({
708
+ id: `wp-${uuid().substring(0, 8)}`,
709
+ question: 'How many months have 28 days?',
710
+ category: 'logic',
711
+ acceptedAnswers: ['12', 'twelve', 'all', 'all of them'],
712
+ }),
713
+ () => ({
714
+ id: `wp-${uuid().substring(0, 8)}`,
715
+ question: 'If you have one, you want to share it. If you share it, you no longer have it. What is it?',
716
+ category: 'logic',
717
+ acceptedAnswers: ['secret', 'a secret'],
718
+ }),
719
+ () => ({
720
+ id: `wp-${uuid().substring(0, 8)}`,
721
+ question: 'What occurs once in a minute, twice in a moment, but never in a thousand years?',
722
+ category: 'wordplay',
723
+ acceptedAnswers: ['m', 'the letter m'],
724
+ }),
725
+ // Technology wordplay
726
+ () => ({
727
+ id: `wp-${uuid().substring(0, 8)}`,
728
+ question: 'What has a screen, keyboard, and mouse but is not alive?',
729
+ category: 'common-sense',
730
+ acceptedAnswers: ['computer', 'a computer', 'pc', 'laptop'],
731
+ }),
732
+ () => ({
733
+ id: `wp-${uuid().substring(0, 8)}`,
734
+ question: 'What connects computers worldwide but has no physical form?',
735
+ category: 'common-sense',
736
+ acceptedAnswers: ['internet', 'the internet', 'web', 'network'],
737
+ }),
738
+ () => ({
739
+ id: `wp-${uuid().substring(0, 8)}`,
740
+ question: 'What do you call a group of 8 bits?',
741
+ category: 'code',
742
+ acceptedAnswers: ['byte', 'a byte'],
743
+ }),
744
+ // Nature/Science wordplay
745
+ () => ({
746
+ id: `wp-${uuid().substring(0, 8)}`,
747
+ question: 'What falls but never breaks, and breaks but never falls?',
748
+ category: 'wordplay',
749
+ acceptedAnswers: ['night and day', 'nightfall and daybreak', 'night falls day breaks'],
750
+ }),
751
+ () => ({
752
+ id: `wp-${uuid().substring(0, 8)}`,
753
+ question: 'What has roots that nobody sees, taller than trees, up up it goes, yet never grows?',
754
+ category: 'common-sense',
755
+ acceptedAnswers: ['mountain', 'a mountain', 'mountains'],
756
+ }),
757
+ () => ({
758
+ id: `wp-${uuid().substring(0, 8)}`,
759
+ question: 'What travels around the world but stays in one corner?',
760
+ category: 'common-sense',
761
+ acceptedAnswers: ['stamp', 'a stamp', 'postage stamp'],
762
+ }),
763
+ () => ({
764
+ id: `wp-${uuid().substring(0, 8)}`,
765
+ question: 'Complete the analogy: Bee is to hive as human is to ___',
766
+ category: 'analogy',
767
+ acceptedAnswers: ['house', 'home', 'city', 'building'],
768
+ }),
769
+ () => ({
770
+ id: `wp-${uuid().substring(0, 8)}`,
771
+ question: 'What has four fingers and a thumb but is not alive?',
772
+ category: 'common-sense',
773
+ acceptedAnswers: ['glove', 'a glove'],
774
+ }),
386
775
  ];
776
+ // All generators, weighted toward parameterized (harder to game)
777
+ const QUESTION_GENERATORS = [
778
+ genMathAdd, genMathMultiply, genMathModulo, genMathSheep, genMathDoubling, genMathMachines,
779
+ genCodeModulo, genCodeBitwise, genCodeStringLen,
780
+ genLogicSyllogism, genLogicNegation, genLogicSequence,
781
+ ...WORDPLAY_GENERATORS,
782
+ ];
783
+ // Generate fresh question bank (unique every call)
784
+ function generateQuestionBank() {
785
+ return QUESTION_GENERATORS.map(gen => gen());
786
+ }
387
787
  /**
388
788
  * Generate a reasoning challenge: 3 random questions requiring LLM capabilities
389
789
  */
390
- export async function generateReasoningChallenge(kv) {
790
+ export async function generateReasoningChallenge(kv, app_id) {
391
791
  cleanExpired();
392
792
  const id = uuid();
393
793
  // Pick 3 random questions from different categories
394
- const shuffled = [...QUESTION_BANK].sort(() => Math.random() - 0.5);
794
+ const freshBank = generateQuestionBank();
795
+ const shuffled = freshBank.sort(() => Math.random() - 0.5);
395
796
  const selectedCategories = new Set();
396
797
  const selectedQuestions = [];
397
798
  for (const q of shuffled) {
@@ -423,6 +824,7 @@ export async function generateReasoningChallenge(kv) {
423
824
  expectedAnswers,
424
825
  issuedAt: Date.now(),
425
826
  expiresAt: Date.now() + timeLimit + 5000,
827
+ app_id,
426
828
  };
427
829
  // Store in KV or memory
428
830
  if (kv) {
@@ -533,18 +935,19 @@ const hybridChallenges = new Map();
533
935
  /**
534
936
  * Generate a hybrid challenge: speed + reasoning combined
535
937
  */
536
- export async function generateHybridChallenge(kv) {
938
+ export async function generateHybridChallenge(kv, clientTimestamp, app_id) {
537
939
  cleanExpired();
538
940
  const id = uuid();
539
- // Generate both sub-challenges
540
- const speedChallenge = await generateSpeedChallenge(kv);
541
- const reasoningChallenge = await generateReasoningChallenge(kv);
941
+ // Generate both sub-challenges (speed with RTT awareness)
942
+ const speedChallenge = await generateSpeedChallenge(kv, clientTimestamp, app_id);
943
+ const reasoningChallenge = await generateReasoningChallenge(kv, app_id);
542
944
  const hybrid = {
543
945
  id,
544
946
  speedChallengeId: speedChallenge.id,
545
947
  reasoningChallengeId: reasoningChallenge.id,
546
948
  issuedAt: Date.now(),
547
949
  expiresAt: Date.now() + 35000,
950
+ app_id,
548
951
  };
549
952
  // Store in KV or memory
550
953
  if (kv) {
@@ -553,6 +956,9 @@ export async function generateHybridChallenge(kv) {
553
956
  else {
554
957
  hybridChallenges.set(id, hybrid);
555
958
  }
959
+ const instructions = speedChallenge.rttInfo
960
+ ? `Solve ALL speed problems (SHA256) in <${speedChallenge.timeLimit}ms (RTT-adjusted) AND answer ALL reasoning questions. Submit both together. Tip: compute all hashes in-process and submit in a single HTTP request.`
961
+ : 'Solve ALL speed problems (SHA256) in <500ms AND answer ALL reasoning questions. Submit both together. Tip: compute all hashes in-process and submit in a single HTTP request.';
556
962
  return {
557
963
  id,
558
964
  speed: {
@@ -563,7 +969,8 @@ export async function generateHybridChallenge(kv) {
563
969
  questions: reasoningChallenge.questions,
564
970
  timeLimit: reasoningChallenge.timeLimit,
565
971
  },
566
- instructions: 'Solve ALL speed problems (SHA256) in <500ms AND answer ALL reasoning questions. Submit both together.',
972
+ instructions,
973
+ rttInfo: speedChallenge.rttInfo,
567
974
  };
568
975
  }
569
976
  /**