@dupecom/botcha 0.13.1 → 0.14.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 (35) hide show
  1. package/README.md +46 -1
  2. package/dist/lib/client/index.d.ts +64 -2
  3. package/dist/lib/client/index.d.ts.map +1 -1
  4. package/dist/lib/client/index.js +136 -1
  5. package/dist/lib/client/types.d.ts +68 -0
  6. package/dist/lib/client/types.d.ts.map +1 -1
  7. package/dist/lib/index.js +2 -0
  8. package/dist/src/challenges/compute.d.ts +19 -0
  9. package/dist/src/challenges/compute.d.ts.map +1 -0
  10. package/dist/src/challenges/compute.js +88 -0
  11. package/dist/src/challenges/hybrid.d.ts +45 -0
  12. package/dist/src/challenges/hybrid.d.ts.map +1 -0
  13. package/dist/src/challenges/hybrid.js +94 -0
  14. package/dist/src/challenges/reasoning.d.ts +29 -0
  15. package/dist/src/challenges/reasoning.d.ts.map +1 -0
  16. package/dist/src/challenges/reasoning.js +414 -0
  17. package/dist/src/challenges/speed.d.ts +34 -0
  18. package/dist/src/challenges/speed.d.ts.map +1 -0
  19. package/dist/src/challenges/speed.js +115 -0
  20. package/dist/src/middleware/tap-enhanced-verify.d.ts +57 -0
  21. package/dist/src/middleware/tap-enhanced-verify.d.ts.map +1 -0
  22. package/dist/src/middleware/tap-enhanced-verify.js +368 -0
  23. package/dist/src/middleware/verify.d.ts +12 -0
  24. package/dist/src/middleware/verify.d.ts.map +1 -0
  25. package/dist/src/middleware/verify.js +141 -0
  26. package/dist/src/utils/badge-image.d.ts +15 -0
  27. package/dist/src/utils/badge-image.d.ts.map +1 -0
  28. package/dist/src/utils/badge-image.js +253 -0
  29. package/dist/src/utils/badge.d.ts +39 -0
  30. package/dist/src/utils/badge.d.ts.map +1 -0
  31. package/dist/src/utils/badge.js +125 -0
  32. package/dist/src/utils/signature.d.ts +23 -0
  33. package/dist/src/utils/signature.d.ts.map +1 -0
  34. package/dist/src/utils/signature.js +160 -0
  35. 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"}