@dupecom/botcha 0.13.1 → 0.15.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 +48 -2
- package/dist/lib/client/index.d.ts +64 -2
- package/dist/lib/client/index.d.ts.map +1 -1
- package/dist/lib/client/index.js +136 -1
- package/dist/lib/client/types.d.ts +68 -0
- package/dist/lib/client/types.d.ts.map +1 -1
- package/dist/lib/index.js +2 -0
- package/dist/src/challenges/compute.d.ts +19 -0
- package/dist/src/challenges/compute.d.ts.map +1 -0
- package/dist/src/challenges/compute.js +88 -0
- package/dist/src/challenges/hybrid.d.ts +45 -0
- package/dist/src/challenges/hybrid.d.ts.map +1 -0
- package/dist/src/challenges/hybrid.js +94 -0
- package/dist/src/challenges/reasoning.d.ts +29 -0
- package/dist/src/challenges/reasoning.d.ts.map +1 -0
- package/dist/src/challenges/reasoning.js +414 -0
- package/dist/src/challenges/speed.d.ts +34 -0
- package/dist/src/challenges/speed.d.ts.map +1 -0
- package/dist/src/challenges/speed.js +115 -0
- package/dist/src/middleware/tap-enhanced-verify.d.ts +57 -0
- package/dist/src/middleware/tap-enhanced-verify.d.ts.map +1 -0
- package/dist/src/middleware/tap-enhanced-verify.js +368 -0
- package/dist/src/middleware/verify.d.ts +12 -0
- package/dist/src/middleware/verify.d.ts.map +1 -0
- package/dist/src/middleware/verify.js +141 -0
- package/dist/src/utils/badge-image.d.ts +15 -0
- package/dist/src/utils/badge-image.d.ts.map +1 -0
- package/dist/src/utils/badge-image.js +253 -0
- package/dist/src/utils/badge.d.ts +39 -0
- package/dist/src/utils/badge.d.ts.map +1 -0
- package/dist/src/utils/badge.js +125 -0
- package/dist/src/utils/signature.d.ts +23 -0
- package/dist/src/utils/signature.d.ts.map +1 -0
- package/dist/src/utils/signature.js +160 -0
- package/package.json +6 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a reasoning challenge: 3 random questions requiring LLM capabilities
|
|
3
|
+
* Simple scripts can't answer these, but any LLM can
|
|
4
|
+
*/
|
|
5
|
+
export declare function generateReasoningChallenge(): {
|
|
6
|
+
id: string;
|
|
7
|
+
questions: {
|
|
8
|
+
id: string;
|
|
9
|
+
question: string;
|
|
10
|
+
category: string;
|
|
11
|
+
}[];
|
|
12
|
+
timeLimit: number;
|
|
13
|
+
instructions: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function verifyReasoningChallenge(id: string, answers: Record<string, string>): {
|
|
16
|
+
valid: boolean;
|
|
17
|
+
reason?: string;
|
|
18
|
+
solveTimeMs?: number;
|
|
19
|
+
correctCount?: number;
|
|
20
|
+
totalCount?: number;
|
|
21
|
+
};
|
|
22
|
+
export declare function getQuestionCount(): number;
|
|
23
|
+
declare const _default: {
|
|
24
|
+
generateReasoningChallenge: typeof generateReasoningChallenge;
|
|
25
|
+
verifyReasoningChallenge: typeof verifyReasoningChallenge;
|
|
26
|
+
getQuestionCount: typeof getQuestionCount;
|
|
27
|
+
};
|
|
28
|
+
export default _default;
|
|
29
|
+
//# sourceMappingURL=reasoning.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reasoning.d.ts","sourceRoot":"","sources":["../../../src/challenges/reasoning.ts"],"names":[],"mappings":"AAiUA;;;GAGG;AACH,wBAAgB,0BAA0B,IAAI;IAC5C,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;CACtB,CAkDA;AAiCD,wBAAgB,wBAAwB,CACtC,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC9B;IACD,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CA0DA;AAED,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;;;;;;AAED,wBAA0F"}
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
const reasoningChallenges = new Map();
|
|
3
|
+
// ============ PARAMETERIZED QUESTION GENERATORS ============
|
|
4
|
+
// These generate unique questions each time, so a static lookup table won't work.
|
|
5
|
+
function randInt(min, max) {
|
|
6
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
7
|
+
}
|
|
8
|
+
function pickRandom(arr) {
|
|
9
|
+
return arr[Math.floor(Math.random() * arr.length)];
|
|
10
|
+
}
|
|
11
|
+
// --- Math generators (randomized numbers each time) ---
|
|
12
|
+
function genMathAdd() {
|
|
13
|
+
const a = randInt(100, 999);
|
|
14
|
+
const b = randInt(100, 999);
|
|
15
|
+
const answer = (a + b).toString();
|
|
16
|
+
return {
|
|
17
|
+
id: `math-add-${crypto.randomUUID().substring(0, 8)}`,
|
|
18
|
+
question: `What is ${a} + ${b}?`,
|
|
19
|
+
category: 'math',
|
|
20
|
+
acceptedAnswers: [answer],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function genMathMultiply() {
|
|
24
|
+
const a = randInt(12, 99);
|
|
25
|
+
const b = randInt(12, 99);
|
|
26
|
+
const answer = (a * b).toString();
|
|
27
|
+
return {
|
|
28
|
+
id: `math-mul-${crypto.randomUUID().substring(0, 8)}`,
|
|
29
|
+
question: `What is ${a} × ${b}?`,
|
|
30
|
+
category: 'math',
|
|
31
|
+
acceptedAnswers: [answer],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function genMathModulo() {
|
|
35
|
+
const a = randInt(50, 999);
|
|
36
|
+
const b = randInt(3, 19);
|
|
37
|
+
const answer = (a % b).toString();
|
|
38
|
+
return {
|
|
39
|
+
id: `math-mod-${crypto.randomUUID().substring(0, 8)}`,
|
|
40
|
+
question: `What is ${a} % ${b} (modulo)?`,
|
|
41
|
+
category: 'math',
|
|
42
|
+
acceptedAnswers: [answer],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function genMathSheep() {
|
|
46
|
+
const total = randInt(15, 50);
|
|
47
|
+
const remaining = randInt(3, total - 2);
|
|
48
|
+
return {
|
|
49
|
+
id: `math-sheep-${crypto.randomUUID().substring(0, 8)}`,
|
|
50
|
+
question: `A farmer has ${total} sheep. All but ${remaining} run away. How many sheep does he have left? Answer with just the number.`,
|
|
51
|
+
category: 'math',
|
|
52
|
+
acceptedAnswers: [remaining.toString()],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function genMathDoubling() {
|
|
56
|
+
// Vary starting value (days: 10-1050) to get 1041 unique answer values
|
|
57
|
+
// Answer is always (days - 1), so days range of 10-1050 gives answers 9-1049
|
|
58
|
+
const days = randInt(10, 1050);
|
|
59
|
+
const items = pickRandom([
|
|
60
|
+
'lily pads',
|
|
61
|
+
'bacteria cells',
|
|
62
|
+
'algae colonies',
|
|
63
|
+
'viral particles',
|
|
64
|
+
'yeast cells',
|
|
65
|
+
'water plants',
|
|
66
|
+
'moss patches',
|
|
67
|
+
'fungal spores',
|
|
68
|
+
]);
|
|
69
|
+
const answer = (days - 1).toString();
|
|
70
|
+
return {
|
|
71
|
+
id: `math-double-${crypto.randomUUID().substring(0, 8)}`,
|
|
72
|
+
question: `A patch of ${items} 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.`,
|
|
73
|
+
category: 'math',
|
|
74
|
+
acceptedAnswers: [answer],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function genMathMachines() {
|
|
78
|
+
// Parameterized: n (5-1100) to get 1096 unique answer values
|
|
79
|
+
// Answer is always n (the time in minutes)
|
|
80
|
+
const n = randInt(5, 1100);
|
|
81
|
+
const m = randInt(50, 500);
|
|
82
|
+
const items = pickRandom([
|
|
83
|
+
'widgets',
|
|
84
|
+
'parts',
|
|
85
|
+
'components',
|
|
86
|
+
'units',
|
|
87
|
+
'products',
|
|
88
|
+
'items',
|
|
89
|
+
'gadgets',
|
|
90
|
+
'devices',
|
|
91
|
+
]);
|
|
92
|
+
return {
|
|
93
|
+
id: `math-machines-${crypto.randomUUID().substring(0, 8)}`,
|
|
94
|
+
question: `If it takes ${n} machines ${n} minutes to make ${n} ${items}, how many minutes would it take ${m} machines to make ${m} ${items}? Answer with just the number.`,
|
|
95
|
+
category: 'math',
|
|
96
|
+
acceptedAnswers: [n.toString()],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
// --- Code generators (randomized values) ---
|
|
100
|
+
function genCodeModulo() {
|
|
101
|
+
const a = randInt(20, 200);
|
|
102
|
+
const b = randInt(3, 15);
|
|
103
|
+
const answer = (a % b).toString();
|
|
104
|
+
return {
|
|
105
|
+
id: `code-mod-${crypto.randomUUID().substring(0, 8)}`,
|
|
106
|
+
question: `In most programming languages, what does ${a} % ${b} evaluate to?`,
|
|
107
|
+
category: 'code',
|
|
108
|
+
acceptedAnswers: [answer],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function genCodeBitwise() {
|
|
112
|
+
const a = randInt(1, 15);
|
|
113
|
+
const b = randInt(1, 15);
|
|
114
|
+
const op = pickRandom(['&', '|', '^']);
|
|
115
|
+
const opName = op === '&' ? 'AND' : op === '|' ? 'OR' : 'XOR';
|
|
116
|
+
let answer;
|
|
117
|
+
if (op === '&')
|
|
118
|
+
answer = a & b;
|
|
119
|
+
else if (op === '|')
|
|
120
|
+
answer = a | b;
|
|
121
|
+
else
|
|
122
|
+
answer = a ^ b;
|
|
123
|
+
return {
|
|
124
|
+
id: `code-bit-${crypto.randomUUID().substring(0, 8)}`,
|
|
125
|
+
question: `What is ${a} ${op} ${b} (bitwise ${opName})? Answer with just the number.`,
|
|
126
|
+
category: 'code',
|
|
127
|
+
acceptedAnswers: [answer.toString()],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function genCodeStringLen() {
|
|
131
|
+
const words = ['hello', 'world', 'banana', 'algorithm', 'quantum', 'symphony', 'cryptography', 'paradigm', 'ephemeral', 'serendipity'];
|
|
132
|
+
const word = pickRandom(words);
|
|
133
|
+
return {
|
|
134
|
+
id: `code-strlen-${crypto.randomUUID().substring(0, 8)}`,
|
|
135
|
+
question: `What is the length of the string "${word}"? Answer with just the number.`,
|
|
136
|
+
category: 'code',
|
|
137
|
+
acceptedAnswers: [word.length.toString()],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// --- Logic generators (randomized names/items) ---
|
|
141
|
+
function genLogicSyllogism() {
|
|
142
|
+
// Generate counting-based syllogism to produce numeric answers with ≥1000 answer space
|
|
143
|
+
// "In a group of N people, if X are teachers and all teachers are kind, how many kind people are there at minimum?"
|
|
144
|
+
// Answer: X (the number in the subset)
|
|
145
|
+
// Range: X from 10 to 1500 = 1491 unique answers
|
|
146
|
+
const total = randInt(50, 2000);
|
|
147
|
+
const count = randInt(10, Math.min(total - 5, 1500));
|
|
148
|
+
const groups = [
|
|
149
|
+
{ container: 'people', subsetName: 'teachers', property: 'kind' },
|
|
150
|
+
{ container: 'animals', subsetName: 'dogs', property: 'friendly' },
|
|
151
|
+
{ container: 'students', subsetName: 'seniors', property: 'experienced' },
|
|
152
|
+
{ container: 'employees', subsetName: 'managers', property: 'trained' },
|
|
153
|
+
{ container: 'books', subsetName: 'novels', property: 'fictional' },
|
|
154
|
+
{ container: 'vehicles', subsetName: 'cars', property: 'motorized' },
|
|
155
|
+
{ container: 'plants', subsetName: 'trees', property: 'woody' },
|
|
156
|
+
{ container: 'items', subsetName: 'tools', property: 'useful' },
|
|
157
|
+
];
|
|
158
|
+
const { container, subsetName, property } = pickRandom(groups);
|
|
159
|
+
return {
|
|
160
|
+
id: `logic-syl-${crypto.randomUUID().substring(0, 8)}`,
|
|
161
|
+
question: `In a group of ${total} ${container}, ${count} of them are ${subsetName}. If all ${subsetName} are ${property}, what is the minimum number of ${property} ${container}? Answer with just the number.`,
|
|
162
|
+
category: 'logic',
|
|
163
|
+
acceptedAnswers: [count.toString()],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function genLogicNegation() {
|
|
167
|
+
const total = randInt(20, 100);
|
|
168
|
+
const keep = randInt(3, total - 5);
|
|
169
|
+
return {
|
|
170
|
+
id: `logic-neg-${crypto.randomUUID().substring(0, 8)}`,
|
|
171
|
+
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.`,
|
|
172
|
+
category: 'logic',
|
|
173
|
+
acceptedAnswers: [keep.toString()],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function genLogicSequence() {
|
|
177
|
+
// Generate a simple arithmetic or geometric sequence
|
|
178
|
+
const start = randInt(2, 20);
|
|
179
|
+
const step = randInt(2, 8);
|
|
180
|
+
const seq = [start, start + step, start + 2 * step, start + 3 * step];
|
|
181
|
+
const answer = (start + 4 * step).toString();
|
|
182
|
+
return {
|
|
183
|
+
id: `logic-seq-${crypto.randomUUID().substring(0, 8)}`,
|
|
184
|
+
question: `What comes next in the sequence: ${seq.join(', ')}, ___? Answer with just the number.`,
|
|
185
|
+
category: 'logic',
|
|
186
|
+
acceptedAnswers: [answer],
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
// --- Wordplay (static but with large pool + randomized IDs so lookup by ID fails) ---
|
|
190
|
+
const WORDPLAY_QUESTIONS = [
|
|
191
|
+
() => ({
|
|
192
|
+
id: `wp-${crypto.randomUUID().substring(0, 8)}`,
|
|
193
|
+
question: 'What single word connects: apple, Newton, gravity?',
|
|
194
|
+
category: 'wordplay',
|
|
195
|
+
acceptedAnswers: ['tree', 'fall', 'falling'],
|
|
196
|
+
}),
|
|
197
|
+
() => ({
|
|
198
|
+
id: `wp-${crypto.randomUUID().substring(0, 8)}`,
|
|
199
|
+
question: 'What single word connects: key, piano, computer?',
|
|
200
|
+
category: 'wordplay',
|
|
201
|
+
acceptedAnswers: ['keyboard', 'board', 'keys'],
|
|
202
|
+
}),
|
|
203
|
+
() => ({
|
|
204
|
+
id: `wp-${crypto.randomUUID().substring(0, 8)}`,
|
|
205
|
+
question: 'What single word connects: river, money, blood?',
|
|
206
|
+
category: 'wordplay',
|
|
207
|
+
acceptedAnswers: ['bank', 'flow', 'stream'],
|
|
208
|
+
}),
|
|
209
|
+
() => ({
|
|
210
|
+
id: `wp-${crypto.randomUUID().substring(0, 8)}`,
|
|
211
|
+
question: 'What word can precede: light, house, shine?',
|
|
212
|
+
category: 'wordplay',
|
|
213
|
+
acceptedAnswers: ['sun', 'moon'],
|
|
214
|
+
}),
|
|
215
|
+
() => ({
|
|
216
|
+
id: `wp-${crypto.randomUUID().substring(0, 8)}`,
|
|
217
|
+
question: 'What gets wetter the more it dries?',
|
|
218
|
+
category: 'common-sense',
|
|
219
|
+
acceptedAnswers: ['towel', 'a towel', 'cloth', 'rag'],
|
|
220
|
+
}),
|
|
221
|
+
() => ({
|
|
222
|
+
id: `wp-${crypto.randomUUID().substring(0, 8)}`,
|
|
223
|
+
question: 'What can you catch but not throw?',
|
|
224
|
+
category: 'common-sense',
|
|
225
|
+
acceptedAnswers: ['cold', 'a cold', 'breath', 'your breath', 'feelings', 'disease'],
|
|
226
|
+
}),
|
|
227
|
+
() => ({
|
|
228
|
+
id: `wp-${crypto.randomUUID().substring(0, 8)}`,
|
|
229
|
+
question: 'What data structure uses LIFO (Last In, First Out)?',
|
|
230
|
+
category: 'code',
|
|
231
|
+
acceptedAnswers: ['stack', 'a stack'],
|
|
232
|
+
}),
|
|
233
|
+
() => ({
|
|
234
|
+
id: `wp-${crypto.randomUUID().substring(0, 8)}`,
|
|
235
|
+
question: 'Complete the analogy: Fish is to water as bird is to ___',
|
|
236
|
+
category: 'analogy',
|
|
237
|
+
acceptedAnswers: ['air', 'sky', 'atmosphere'],
|
|
238
|
+
}),
|
|
239
|
+
() => ({
|
|
240
|
+
id: `wp-${crypto.randomUUID().substring(0, 8)}`,
|
|
241
|
+
question: 'Complete the analogy: Eye is to see as ear is to ___',
|
|
242
|
+
category: 'analogy',
|
|
243
|
+
acceptedAnswers: ['hear', 'listen', 'hearing', 'listening'],
|
|
244
|
+
}),
|
|
245
|
+
() => ({
|
|
246
|
+
id: `wp-${crypto.randomUUID().substring(0, 8)}`,
|
|
247
|
+
question: 'Complete the analogy: Painter is to brush as writer is to ___',
|
|
248
|
+
category: 'analogy',
|
|
249
|
+
acceptedAnswers: ['pen', 'pencil', 'keyboard', 'typewriter', 'quill'],
|
|
250
|
+
}),
|
|
251
|
+
];
|
|
252
|
+
// All generators, weighted toward parameterized (harder to game)
|
|
253
|
+
const QUESTION_GENERATORS = [
|
|
254
|
+
// Math (parameterized — unique every time)
|
|
255
|
+
genMathAdd,
|
|
256
|
+
genMathMultiply,
|
|
257
|
+
genMathModulo,
|
|
258
|
+
genMathSheep,
|
|
259
|
+
genMathDoubling,
|
|
260
|
+
genMathMachines,
|
|
261
|
+
// Code (parameterized)
|
|
262
|
+
genCodeModulo,
|
|
263
|
+
genCodeBitwise,
|
|
264
|
+
genCodeStringLen,
|
|
265
|
+
// Logic (parameterized)
|
|
266
|
+
genLogicSyllogism,
|
|
267
|
+
genLogicNegation,
|
|
268
|
+
genLogicSequence,
|
|
269
|
+
// Wordplay / static (but with random IDs)
|
|
270
|
+
...WORDPLAY_QUESTIONS,
|
|
271
|
+
];
|
|
272
|
+
// Legacy compatibility: generate a static-looking bank from generators
|
|
273
|
+
function generateQuestionBank() {
|
|
274
|
+
return QUESTION_GENERATORS.map(gen => gen());
|
|
275
|
+
}
|
|
276
|
+
// Cleanup expired challenges
|
|
277
|
+
setInterval(() => {
|
|
278
|
+
const now = Date.now();
|
|
279
|
+
for (const [id, c] of reasoningChallenges) {
|
|
280
|
+
if (c.expiresAt < now)
|
|
281
|
+
reasoningChallenges.delete(id);
|
|
282
|
+
}
|
|
283
|
+
}, 60000);
|
|
284
|
+
/**
|
|
285
|
+
* Generate a reasoning challenge: 3 random questions requiring LLM capabilities
|
|
286
|
+
* Simple scripts can't answer these, but any LLM can
|
|
287
|
+
*/
|
|
288
|
+
export function generateReasoningChallenge() {
|
|
289
|
+
const id = crypto.randomUUID();
|
|
290
|
+
// Generate fresh parameterized questions (different every time)
|
|
291
|
+
const freshBank = generateQuestionBank();
|
|
292
|
+
const shuffled = freshBank.sort(() => Math.random() - 0.5);
|
|
293
|
+
const selectedCategories = new Set();
|
|
294
|
+
const selectedQuestions = [];
|
|
295
|
+
for (const q of shuffled) {
|
|
296
|
+
if (selectedQuestions.length >= 3)
|
|
297
|
+
break;
|
|
298
|
+
// Try to get diverse categories
|
|
299
|
+
if (selectedQuestions.length < 2 || !selectedCategories.has(q.category)) {
|
|
300
|
+
selectedQuestions.push(q);
|
|
301
|
+
selectedCategories.add(q.category);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// If we didn't get 3, just take any 3
|
|
305
|
+
while (selectedQuestions.length < 3 && shuffled.length > selectedQuestions.length) {
|
|
306
|
+
const q = shuffled.find(sq => !selectedQuestions.includes(sq));
|
|
307
|
+
if (q)
|
|
308
|
+
selectedQuestions.push(q);
|
|
309
|
+
}
|
|
310
|
+
const expectedAnswers = new Map();
|
|
311
|
+
const questions = selectedQuestions.map(q => {
|
|
312
|
+
expectedAnswers.set(q.id, q.acceptedAnswers);
|
|
313
|
+
return {
|
|
314
|
+
id: q.id,
|
|
315
|
+
question: q.question,
|
|
316
|
+
category: q.category,
|
|
317
|
+
};
|
|
318
|
+
});
|
|
319
|
+
const timeLimit = 30000; // 30 seconds - plenty of time for an LLM
|
|
320
|
+
reasoningChallenges.set(id, {
|
|
321
|
+
id,
|
|
322
|
+
questions,
|
|
323
|
+
expectedAnswers,
|
|
324
|
+
issuedAt: Date.now(),
|
|
325
|
+
expiresAt: Date.now() + timeLimit + 5000, // 5s grace for network latency
|
|
326
|
+
});
|
|
327
|
+
return {
|
|
328
|
+
id,
|
|
329
|
+
questions,
|
|
330
|
+
timeLimit,
|
|
331
|
+
instructions: 'Answer all 3 questions. These require reasoning that LLMs can do but simple scripts cannot. You have 30 seconds.',
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Normalize answer for comparison
|
|
336
|
+
*/
|
|
337
|
+
function normalizeAnswer(answer) {
|
|
338
|
+
return answer
|
|
339
|
+
.toLowerCase()
|
|
340
|
+
.trim()
|
|
341
|
+
.replace(/[.,!?'"]/g, '') // Remove punctuation
|
|
342
|
+
.replace(/\s+/g, ' '); // Normalize whitespace
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Check if an answer matches any accepted answer
|
|
346
|
+
*/
|
|
347
|
+
function isAnswerAccepted(answer, acceptedAnswers) {
|
|
348
|
+
const normalized = normalizeAnswer(answer);
|
|
349
|
+
for (const accepted of acceptedAnswers) {
|
|
350
|
+
const normalizedAccepted = normalizeAnswer(accepted);
|
|
351
|
+
// Exact match
|
|
352
|
+
if (normalized === normalizedAccepted)
|
|
353
|
+
return true;
|
|
354
|
+
// Contains match (for longer answers that include the key word)
|
|
355
|
+
if (normalized.includes(normalizedAccepted))
|
|
356
|
+
return true;
|
|
357
|
+
if (normalizedAccepted.includes(normalized) && normalized.length > 2)
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
export function verifyReasoningChallenge(id, answers) {
|
|
363
|
+
const challenge = reasoningChallenges.get(id);
|
|
364
|
+
if (!challenge) {
|
|
365
|
+
return { valid: false, reason: 'Challenge not found or expired' };
|
|
366
|
+
}
|
|
367
|
+
const now = Date.now();
|
|
368
|
+
const solveTimeMs = now - challenge.issuedAt;
|
|
369
|
+
// Clean up
|
|
370
|
+
reasoningChallenges.delete(id);
|
|
371
|
+
if (now > challenge.expiresAt) {
|
|
372
|
+
return { valid: false, reason: `Too slow! Took ${solveTimeMs}ms, limit was 30 seconds` };
|
|
373
|
+
}
|
|
374
|
+
if (!answers || typeof answers !== 'object') {
|
|
375
|
+
return { valid: false, reason: 'Answers must be an object mapping question IDs to answers' };
|
|
376
|
+
}
|
|
377
|
+
let correctCount = 0;
|
|
378
|
+
const totalCount = challenge.questions.length;
|
|
379
|
+
const wrongQuestions = [];
|
|
380
|
+
for (const q of challenge.questions) {
|
|
381
|
+
const userAnswer = answers[q.id];
|
|
382
|
+
const acceptedAnswers = challenge.expectedAnswers.get(q.id) || [];
|
|
383
|
+
if (!userAnswer) {
|
|
384
|
+
wrongQuestions.push(q.id);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (isAnswerAccepted(userAnswer, acceptedAnswers)) {
|
|
388
|
+
correctCount++;
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
wrongQuestions.push(q.id);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// Require all 3 correct
|
|
395
|
+
if (correctCount < totalCount) {
|
|
396
|
+
return {
|
|
397
|
+
valid: false,
|
|
398
|
+
reason: `Only ${correctCount}/${totalCount} correct. Wrong: ${wrongQuestions.join(', ')}`,
|
|
399
|
+
solveTimeMs,
|
|
400
|
+
correctCount,
|
|
401
|
+
totalCount,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
valid: true,
|
|
406
|
+
solveTimeMs,
|
|
407
|
+
correctCount,
|
|
408
|
+
totalCount,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
export function getQuestionCount() {
|
|
412
|
+
return QUESTION_GENERATORS.length;
|
|
413
|
+
}
|
|
414
|
+
export default { generateReasoningChallenge, verifyReasoningChallenge, getQuestionCount };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a speed challenge: 5 math problems, RTT-aware timeout
|
|
3
|
+
* Trivial for AI, impossible for humans to copy-paste fast enough
|
|
4
|
+
*/
|
|
5
|
+
export declare function generateSpeedChallenge(clientTimestamp?: number): {
|
|
6
|
+
id: string;
|
|
7
|
+
challenges: {
|
|
8
|
+
num: number;
|
|
9
|
+
operation: string;
|
|
10
|
+
}[];
|
|
11
|
+
timeLimit: number;
|
|
12
|
+
instructions: string;
|
|
13
|
+
rttInfo?: {
|
|
14
|
+
measuredRtt: number;
|
|
15
|
+
adjustedTimeout: number;
|
|
16
|
+
explanation: string;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
export declare function verifySpeedChallenge(id: string, answers: string[]): {
|
|
20
|
+
valid: boolean;
|
|
21
|
+
reason?: string;
|
|
22
|
+
solveTimeMs?: number;
|
|
23
|
+
rttInfo?: {
|
|
24
|
+
measuredRtt: number;
|
|
25
|
+
adjustedTimeout: number;
|
|
26
|
+
actualTime: number;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
declare const _default: {
|
|
30
|
+
generateSpeedChallenge: typeof generateSpeedChallenge;
|
|
31
|
+
verifySpeedChallenge: typeof verifySpeedChallenge;
|
|
32
|
+
};
|
|
33
|
+
export default _default;
|
|
34
|
+
//# sourceMappingURL=speed.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"speed.d.ts","sourceRoot":"","sources":["../../../src/challenges/speed.ts"],"names":[],"mappings":"AAuBA;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG;IAChE,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE;QACR,WAAW,EAAE,MAAM,CAAC;QACpB,eAAe,EAAE,MAAM,CAAC;QACxB,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC;CACH,CAiEA;AAED,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG;IACnE,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE;QACR,WAAW,EAAE,MAAM,CAAC;QACpB,eAAe,EAAE,MAAM,CAAC;QACxB,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;CACH,CAkDA;;;;;AAED,wBAAgE"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
const speedChallenges = new Map();
|
|
3
|
+
// Cleanup expired
|
|
4
|
+
setInterval(() => {
|
|
5
|
+
const now = Date.now();
|
|
6
|
+
for (const [id, c] of speedChallenges) {
|
|
7
|
+
if (c.expiresAt < now)
|
|
8
|
+
speedChallenges.delete(id);
|
|
9
|
+
}
|
|
10
|
+
}, 30000);
|
|
11
|
+
/**
|
|
12
|
+
* Generate a speed challenge: 5 math problems, RTT-aware timeout
|
|
13
|
+
* Trivial for AI, impossible for humans to copy-paste fast enough
|
|
14
|
+
*/
|
|
15
|
+
export function generateSpeedChallenge(clientTimestamp) {
|
|
16
|
+
const id = crypto.randomUUID();
|
|
17
|
+
const challenges = [];
|
|
18
|
+
const expectedAnswers = [];
|
|
19
|
+
for (let i = 0; i < 5; i++) {
|
|
20
|
+
const num = Math.floor(Math.random() * 1000000) + 100000;
|
|
21
|
+
const operation = 'sha256_first8';
|
|
22
|
+
challenges.push({ num, operation });
|
|
23
|
+
const hash = crypto.createHash('sha256').update(num.toString()).digest('hex');
|
|
24
|
+
expectedAnswers.push(hash.substring(0, 8));
|
|
25
|
+
}
|
|
26
|
+
// RTT-aware timeout calculation
|
|
27
|
+
const baseTimeLimit = 500; // Base computation time for AI agents
|
|
28
|
+
const MAX_RTT_MS = 5000; // Cap RTT to prevent timestamp spoofing (5s max)
|
|
29
|
+
const MAX_TIMESTAMP_AGE_MS = 30000; // Reject timestamps older than 30s
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
let rttMs = 0;
|
|
32
|
+
let adjustedTimeLimit = baseTimeLimit;
|
|
33
|
+
let rttInfo = undefined;
|
|
34
|
+
if (clientTimestamp && clientTimestamp > 0) {
|
|
35
|
+
// Reject timestamps in the future or too far in the past (anti-spoofing)
|
|
36
|
+
const age = now - clientTimestamp;
|
|
37
|
+
if (age >= 0 && age <= MAX_TIMESTAMP_AGE_MS) {
|
|
38
|
+
// Calculate RTT from client timestamp, capped to prevent abuse
|
|
39
|
+
rttMs = Math.min(age, MAX_RTT_MS);
|
|
40
|
+
// Adjust timeout: base + (2 * RTT) + 100ms buffer
|
|
41
|
+
// The 2x RTT accounts for request + response network time
|
|
42
|
+
adjustedTimeLimit = Math.max(baseTimeLimit, baseTimeLimit + (2 * rttMs) + 100);
|
|
43
|
+
rttInfo = {
|
|
44
|
+
measuredRtt: rttMs,
|
|
45
|
+
adjustedTimeout: adjustedTimeLimit,
|
|
46
|
+
explanation: `RTT: ${rttMs}ms → Timeout: ${baseTimeLimit}ms + (2×${rttMs}ms) + 100ms = ${adjustedTimeLimit}ms`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// else: invalid timestamp silently ignored, use base timeout
|
|
50
|
+
}
|
|
51
|
+
speedChallenges.set(id, {
|
|
52
|
+
id,
|
|
53
|
+
challenges,
|
|
54
|
+
expectedAnswers,
|
|
55
|
+
issuedAt: now,
|
|
56
|
+
expiresAt: now + adjustedTimeLimit + 50, // Small server-side grace period
|
|
57
|
+
baseTimeLimit,
|
|
58
|
+
adjustedTimeLimit,
|
|
59
|
+
rttMs,
|
|
60
|
+
});
|
|
61
|
+
const instructions = rttMs > 0
|
|
62
|
+
? `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).`
|
|
63
|
+
: 'Compute SHA256 of each number, return first 8 hex chars of each. Submit as array. You have 500ms.';
|
|
64
|
+
return {
|
|
65
|
+
id,
|
|
66
|
+
challenges,
|
|
67
|
+
timeLimit: adjustedTimeLimit,
|
|
68
|
+
instructions,
|
|
69
|
+
rttInfo,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export function verifySpeedChallenge(id, answers) {
|
|
73
|
+
const challenge = speedChallenges.get(id);
|
|
74
|
+
if (!challenge) {
|
|
75
|
+
return { valid: false, reason: 'Challenge not found or expired' };
|
|
76
|
+
}
|
|
77
|
+
const now = Date.now();
|
|
78
|
+
const solveTimeMs = now - challenge.issuedAt;
|
|
79
|
+
// Clean up
|
|
80
|
+
speedChallenges.delete(id);
|
|
81
|
+
// Use the challenge's adjusted timeout, fallback to base if not available
|
|
82
|
+
const timeLimit = challenge.adjustedTimeLimit || challenge.baseTimeLimit || 500;
|
|
83
|
+
if (now > challenge.expiresAt) {
|
|
84
|
+
const rttExplanation = challenge.rttMs
|
|
85
|
+
? ` (RTT-adjusted: ${challenge.rttMs}ms network + ${challenge.baseTimeLimit}ms compute = ${timeLimit}ms limit)`
|
|
86
|
+
: '';
|
|
87
|
+
return {
|
|
88
|
+
valid: false,
|
|
89
|
+
reason: `Too slow! Took ${solveTimeMs}ms, limit was ${timeLimit}ms${rttExplanation}`,
|
|
90
|
+
rttInfo: challenge.rttMs ? {
|
|
91
|
+
measuredRtt: challenge.rttMs,
|
|
92
|
+
adjustedTimeout: timeLimit,
|
|
93
|
+
actualTime: solveTimeMs,
|
|
94
|
+
} : undefined,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (!Array.isArray(answers) || answers.length !== 5) {
|
|
98
|
+
return { valid: false, reason: 'Must provide exactly 5 answers as array' };
|
|
99
|
+
}
|
|
100
|
+
for (let i = 0; i < 5; i++) {
|
|
101
|
+
if (answers[i]?.toLowerCase() !== challenge.expectedAnswers[i]) {
|
|
102
|
+
return { valid: false, reason: `Wrong answer for challenge ${i + 1}` };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
valid: true,
|
|
107
|
+
solveTimeMs,
|
|
108
|
+
rttInfo: challenge.rttMs ? {
|
|
109
|
+
measuredRtt: challenge.rttMs,
|
|
110
|
+
adjustedTimeout: timeLimit,
|
|
111
|
+
actualTime: solveTimeMs,
|
|
112
|
+
} : undefined,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
export default { generateSpeedChallenge, verifySpeedChallenge };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TAP-Enhanced BOTCHA Verification Middleware
|
|
3
|
+
* Extends existing BOTCHA middleware with Trusted Agent Protocol support
|
|
4
|
+
*
|
|
5
|
+
* Provides multiple verification modes:
|
|
6
|
+
* - TAP (full): Cryptographic signature + computational challenge
|
|
7
|
+
* - Signature-only: Cryptographic verification without challenge
|
|
8
|
+
* - Challenge-only: Existing BOTCHA computational challenge
|
|
9
|
+
* - Flexible: TAP preferred but allows fallback
|
|
10
|
+
*/
|
|
11
|
+
import { Request, Response, NextFunction } from 'express';
|
|
12
|
+
export interface TAPBotchaOptions {
|
|
13
|
+
requireSignature?: boolean;
|
|
14
|
+
allowChallenge?: boolean;
|
|
15
|
+
challengeType?: 'standard' | 'speed';
|
|
16
|
+
challengeDifficulty?: 'easy' | 'medium' | 'hard';
|
|
17
|
+
customVerify?: (req: Request) => Promise<boolean>;
|
|
18
|
+
requireTAP?: boolean;
|
|
19
|
+
preferTAP?: boolean;
|
|
20
|
+
tapEnabled?: boolean;
|
|
21
|
+
auditLogging?: boolean;
|
|
22
|
+
trustedIssuers?: string[];
|
|
23
|
+
maxSessionDuration?: number;
|
|
24
|
+
signatureAlgorithms?: string[];
|
|
25
|
+
requireCapabilities?: string[];
|
|
26
|
+
agentsKV?: any;
|
|
27
|
+
sessionsKV?: any;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Enhanced BOTCHA middleware with TAP support
|
|
31
|
+
*/
|
|
32
|
+
export declare function tapEnhancedVerify(options?: TAPBotchaOptions): (req: Request, res: Response, next: NextFunction) => Promise<void | Response<any, Record<string, any>>>;
|
|
33
|
+
export declare const tapVerifyModes: {
|
|
34
|
+
/**
|
|
35
|
+
* Require full TAP authentication (crypto + challenge)
|
|
36
|
+
*/
|
|
37
|
+
strict: (options?: Partial<TAPBotchaOptions>) => (req: Request, res: Response, next: NextFunction) => Promise<void | Response<any, Record<string, any>>>;
|
|
38
|
+
/**
|
|
39
|
+
* Prefer TAP but allow computational fallback
|
|
40
|
+
*/
|
|
41
|
+
flexible: (options?: Partial<TAPBotchaOptions>) => (req: Request, res: Response, next: NextFunction) => Promise<void | Response<any, Record<string, any>>>;
|
|
42
|
+
/**
|
|
43
|
+
* Signature-only verification (no computational challenge)
|
|
44
|
+
*/
|
|
45
|
+
signatureOnly: (options?: Partial<TAPBotchaOptions>) => (req: Request, res: Response, next: NextFunction) => Promise<void | Response<any, Record<string, any>>>;
|
|
46
|
+
/**
|
|
47
|
+
* Development mode with relaxed security
|
|
48
|
+
*/
|
|
49
|
+
development: (options?: Partial<TAPBotchaOptions>) => (req: Request, res: Response, next: NextFunction) => Promise<void | Response<any, Record<string, any>>>;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Alias for tapEnhancedVerify for backward compatibility with docs.
|
|
53
|
+
* Docs reference: import { createTAPVerifyMiddleware } from '@dupecom/botcha/middleware'
|
|
54
|
+
*/
|
|
55
|
+
export declare const createTAPVerifyMiddleware: typeof tapEnhancedVerify;
|
|
56
|
+
export default tapEnhancedVerify;
|
|
57
|
+
//# sourceMappingURL=tap-enhanced-verify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tap-enhanced-verify.d.ts","sourceRoot":"","sources":["../../../src/middleware/tap-enhanced-verify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAoB1D,MAAM,WAAW,gBAAgB;IAE/B,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC;IACrC,mBAAmB,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;IACjD,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAGlD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,YAAY,CAAC,EAAE,OAAO,CAAC;IAGvB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAG/B,QAAQ,CAAC,EAAE,GAAG,CAAC;IACf,UAAU,CAAC,EAAE,GAAG,CAAC;CAClB;AAsBD;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,gBAAqB,IAGhD,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,wDAqE9D;AA8SD,eAAO,MAAM,cAAc;IACzB;;OAEG;uBACe,OAAO,CAAC,gBAAgB,CAAC,WAvXxB,OAAO,OAAO,QAAQ,QAAQ,YAAY;IA8X7D;;OAEG;yBACiB,OAAO,CAAC,gBAAgB,CAAC,WAjY1B,OAAO,OAAO,QAAQ,QAAQ,YAAY;IAwY7D;;OAEG;8BACsB,OAAO,CAAC,gBAAgB,CAAC,WA3Y/B,OAAO,OAAO,QAAQ,QAAQ,YAAY;IAkZ7D;;OAEG;4BACoB,OAAO,CAAC,gBAAgB,CAAC,WArZ7B,OAAO,OAAO,QAAQ,QAAQ,YAAY;CA4Z9D,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,yBAAyB,0BAAoB,CAAC;AAE3D,eAAe,iBAAiB,CAAC"}
|