@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.
- package/dist/analytics.d.ts +60 -0
- package/dist/analytics.d.ts.map +1 -0
- package/dist/analytics.js +130 -0
- package/dist/apps.d.ts +159 -0
- package/dist/apps.d.ts.map +1 -0
- package/dist/apps.js +307 -0
- package/dist/auth.d.ts +93 -6
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +251 -9
- package/dist/challenges.d.ts +31 -7
- package/dist/challenges.d.ts.map +1 -1
- package/dist/challenges.js +551 -144
- package/dist/dashboard/api.d.ts +70 -0
- package/dist/dashboard/api.d.ts.map +1 -0
- package/dist/dashboard/api.js +546 -0
- package/dist/dashboard/auth.d.ts +183 -0
- package/dist/dashboard/auth.d.ts.map +1 -0
- package/dist/dashboard/auth.js +401 -0
- package/dist/dashboard/device-code.d.ts +43 -0
- package/dist/dashboard/device-code.d.ts.map +1 -0
- package/dist/dashboard/device-code.js +77 -0
- package/dist/dashboard/index.d.ts +31 -0
- package/dist/dashboard/index.d.ts.map +1 -0
- package/dist/dashboard/index.js +64 -0
- package/dist/dashboard/layout.d.ts +47 -0
- package/dist/dashboard/layout.d.ts.map +1 -0
- package/dist/dashboard/layout.js +38 -0
- package/dist/dashboard/pages.d.ts +11 -0
- package/dist/dashboard/pages.d.ts.map +1 -0
- package/dist/dashboard/pages.js +18 -0
- package/dist/dashboard/styles.d.ts +11 -0
- package/dist/dashboard/styles.d.ts.map +1 -0
- package/dist/dashboard/styles.js +633 -0
- package/dist/email.d.ts +44 -0
- package/dist/email.d.ts.map +1 -0
- package/dist/email.js +119 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +644 -50
- package/dist/rate-limit.d.ts +11 -1
- package/dist/rate-limit.d.ts.map +1 -1
- package/dist/rate-limit.js +13 -2
- package/dist/routes/stream.js +1 -1
- package/dist/static.d.ts +728 -0
- package/dist/static.d.ts.map +1 -0
- package/dist/static.js +818 -0
- package/package.json +1 -1
package/dist/challenges.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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:
|
|
91
|
-
expiresAt:
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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) >
|
|
193
|
-
return { valid: false, error: 'Timestamp
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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:
|
|
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
|
|
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
|
|
972
|
+
instructions,
|
|
973
|
+
rttInfo: speedChallenge.rttInfo,
|
|
567
974
|
};
|
|
568
975
|
}
|
|
569
976
|
/**
|