aap-agent-server 2.0.0 → 2.6.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/challenges.js +318 -109
- package/index.js +24 -3
- package/middleware.js +60 -15
- package/package.json +2 -2
- package/persistence.js +238 -0
- package/ratelimit.js +117 -0
- package/whitelist.js +231 -0
package/challenges.js
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @aap/server - Challenge Generator v2.
|
|
2
|
+
* @aap/server - Challenge Generator v2.6
|
|
3
3
|
*
|
|
4
|
-
* "
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
4
|
+
* "Burst Mode with Entropy Injection"
|
|
5
|
+
* - 7 challenges in 6 seconds (humans cannot pass)
|
|
6
|
+
* - Salt injection prevents caching attacks
|
|
7
|
+
* - Natural language instructions (requires LLM)
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
+
* v2.6 Changes:
|
|
10
|
+
* - BATCH_SIZE: 5 → 7 (more problems = harder for humans)
|
|
11
|
+
* - MAX_RESPONSE_TIME_MS: 8000 → 6000 (tighter window)
|
|
12
|
+
* - Random chance reduced: (1/3)^7 = 0.05%
|
|
13
|
+
*
|
|
14
|
+
* v2.5 Changes:
|
|
15
|
+
* - BATCH_SIZE: 3 → 5
|
|
16
|
+
* - MAX_RESPONSE_TIME_MS: 12000 → 8000
|
|
17
|
+
* - Salt injection in challenges (must be echoed back)
|
|
9
18
|
*/
|
|
10
19
|
|
|
11
20
|
import { createHash } from 'node:crypto';
|
|
@@ -14,12 +23,12 @@ import { createHash } from 'node:crypto';
|
|
|
14
23
|
* Word pools for dynamic challenge generation
|
|
15
24
|
*/
|
|
16
25
|
const WORD_POOLS = {
|
|
17
|
-
animals: ['cat', 'dog', 'rabbit', 'tiger', 'lion', 'elephant', 'giraffe', 'penguin', 'eagle', 'shark'],
|
|
18
|
-
fruits: ['apple', 'banana', 'orange', 'grape', 'strawberry', 'watermelon', 'peach', 'kiwi', 'mango', 'cherry'],
|
|
19
|
-
colors: ['red', 'blue', 'yellow', 'green', 'purple', 'orange', 'pink', 'black', 'white', 'brown'],
|
|
20
|
-
countries: ['Korea', 'Japan', 'USA', 'UK', 'France', 'Germany', 'Australia', 'Canada', 'Brazil', 'India'],
|
|
21
|
-
verbs: ['runs', 'eats', 'sleeps', 'plays', 'works', 'studies', 'travels', 'cooks'],
|
|
22
|
-
adjectives: ['big', 'small', 'fast', 'slow', 'beautiful', 'cute', 'delicious', 'interesting']
|
|
26
|
+
animals: ['cat', 'dog', 'rabbit', 'tiger', 'lion', 'elephant', 'giraffe', 'penguin', 'eagle', 'shark', 'wolf', 'bear', 'fox', 'deer', 'owl'],
|
|
27
|
+
fruits: ['apple', 'banana', 'orange', 'grape', 'strawberry', 'watermelon', 'peach', 'kiwi', 'mango', 'cherry', 'lemon', 'lime', 'pear', 'plum'],
|
|
28
|
+
colors: ['red', 'blue', 'yellow', 'green', 'purple', 'orange', 'pink', 'black', 'white', 'brown', 'gray', 'cyan', 'magenta'],
|
|
29
|
+
countries: ['Korea', 'Japan', 'USA', 'UK', 'France', 'Germany', 'Australia', 'Canada', 'Brazil', 'India', 'Italy', 'Spain', 'Mexico'],
|
|
30
|
+
verbs: ['runs', 'eats', 'sleeps', 'plays', 'works', 'studies', 'travels', 'cooks', 'reads', 'writes', 'sings', 'dances'],
|
|
31
|
+
adjectives: ['big', 'small', 'fast', 'slow', 'beautiful', 'cute', 'delicious', 'interesting', 'bright', 'dark']
|
|
23
32
|
};
|
|
24
33
|
|
|
25
34
|
/**
|
|
@@ -49,34 +58,59 @@ function seededNumber(nonce, offset, min, max) {
|
|
|
49
58
|
return (seed % (max - min + 1)) + min;
|
|
50
59
|
}
|
|
51
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Generate salt from nonce (for entropy injection)
|
|
63
|
+
* @param {string} nonce - Base nonce
|
|
64
|
+
* @param {number} offset - Offset for variation
|
|
65
|
+
* @returns {string} 6-character salt
|
|
66
|
+
*/
|
|
67
|
+
function generateSalt(nonce, offset = 0) {
|
|
68
|
+
return nonce.slice(offset, offset + 6).toUpperCase();
|
|
69
|
+
}
|
|
70
|
+
|
|
52
71
|
/**
|
|
53
72
|
* Challenge type definitions
|
|
54
73
|
*/
|
|
55
74
|
export const CHALLENGE_TYPES = {
|
|
56
75
|
/**
|
|
57
|
-
* Extract entities from natural language sentence
|
|
76
|
+
* Extract entities from natural language sentence (HARD - more distractors)
|
|
58
77
|
*/
|
|
59
78
|
nlp_extract: {
|
|
60
79
|
generate: (nonce) => {
|
|
80
|
+
const salt = generateSalt(nonce, 0);
|
|
61
81
|
const category = ['animals', 'fruits', 'colors'][parseInt(nonce[0], 16) % 3];
|
|
62
82
|
const pool = WORD_POOLS[category];
|
|
63
|
-
const targets = seededSelect(pool, nonce,
|
|
83
|
+
const targets = seededSelect(pool, nonce, 3, 0); // 3 targets now
|
|
84
|
+
const distractorPool = category === 'animals' ? 'fruits' : category === 'fruits' ? 'colors' : 'animals';
|
|
85
|
+
const distractors = seededSelect(WORD_POOLS[distractorPool], nonce, 2, 8);
|
|
64
86
|
const verb = seededSelect(WORD_POOLS.verbs, nonce, 1, 4)[0];
|
|
87
|
+
const adj = seededSelect(WORD_POOLS.adjectives, nonce, 1, 6)[0];
|
|
65
88
|
|
|
66
|
-
|
|
89
|
+
// Complex sentence with distractors mixed in
|
|
90
|
+
const templates = [
|
|
91
|
+
`The ${adj} ${targets[0]}, a ${distractors[0]}, the ${targets[1]}, and ${targets[2]} all ${verb} near the ${distractors[1]}.`,
|
|
92
|
+
`I saw ${targets[0]} and ${distractors[0]} yesterday, but today only ${targets[1]}, ${targets[2]}, and a ${distractors[1]} appeared.`,
|
|
93
|
+
`Between the ${distractors[0]} and ${distractors[1]}, there were ${targets[0]}, ${targets[1]}, and ${targets[2]} ${verb}ing.`
|
|
94
|
+
];
|
|
95
|
+
const sentence = templates[parseInt(nonce[12], 16) % templates.length];
|
|
67
96
|
|
|
68
97
|
return {
|
|
69
|
-
challenge_string: `Extract
|
|
98
|
+
challenge_string: `[REQ-${salt}] Extract ONLY the ${category} from this sentence (ignore other categories).
|
|
70
99
|
Sentence: "${sentence}"
|
|
71
|
-
Response format: {"items": ["item1", "item2"]}`,
|
|
72
|
-
expected: targets.sort(),
|
|
100
|
+
Response format: {"salt": "${salt}", "items": ["item1", "item2", "item3"]}`,
|
|
101
|
+
expected: { salt, items: targets.map(s => s.toLowerCase()).sort() },
|
|
73
102
|
validate: (solution) => {
|
|
74
103
|
try {
|
|
75
104
|
const match = solution.match(/\{[\s\S]*\}/);
|
|
76
105
|
if (!match) return false;
|
|
77
106
|
const obj = JSON.parse(match[0]);
|
|
107
|
+
|
|
108
|
+
// Check salt
|
|
109
|
+
if (obj.salt !== salt) return false;
|
|
110
|
+
|
|
78
111
|
const items = (obj.items || obj.animals || obj.fruits || obj.colors || []).map(s => s.toLowerCase()).sort();
|
|
79
|
-
|
|
112
|
+
const expected = targets.map(s => s.toLowerCase()).sort();
|
|
113
|
+
return JSON.stringify(items) === JSON.stringify(expected);
|
|
80
114
|
} catch { return false; }
|
|
81
115
|
}
|
|
82
116
|
};
|
|
@@ -84,41 +118,59 @@ Response format: {"items": ["item1", "item2"]}`,
|
|
|
84
118
|
},
|
|
85
119
|
|
|
86
120
|
/**
|
|
87
|
-
* Math problem expressed in natural language
|
|
121
|
+
* Math problem expressed in natural language (EXTREME)
|
|
88
122
|
*/
|
|
89
123
|
nlp_math: {
|
|
90
124
|
generate: (nonce) => {
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
const
|
|
125
|
+
const salt = generateSalt(nonce, 2);
|
|
126
|
+
const a = seededNumber(nonce, 0, 50, 200);
|
|
127
|
+
const b = seededNumber(nonce, 2, 10, 50);
|
|
128
|
+
const c = seededNumber(nonce, 4, 2, 9);
|
|
129
|
+
const d = seededNumber(nonce, 6, 5, 25);
|
|
130
|
+
const e = seededNumber(nonce, 8, 2, 6);
|
|
94
131
|
|
|
95
132
|
const templates = [
|
|
96
133
|
{
|
|
97
|
-
text: `Subtract ${b}
|
|
98
|
-
answer: (a - b) * c
|
|
134
|
+
text: `Start with ${a}. Subtract ${b}. Multiply that by ${c}. Divide the result by ${e}. Finally, add ${d}. What's the final value?`,
|
|
135
|
+
answer: (((a - b) * c) / e) + d
|
|
99
136
|
},
|
|
100
137
|
{
|
|
101
|
-
text: `
|
|
102
|
-
answer: (a + b) /
|
|
138
|
+
text: `Compute: ((${a} + ${b}) × ${c} - ${d}) ÷ ${e}. Give the result rounded to two decimal places.`,
|
|
139
|
+
answer: ((a + b) * c - d) / e
|
|
103
140
|
},
|
|
104
141
|
{
|
|
105
|
-
text: `
|
|
106
|
-
answer: a
|
|
142
|
+
text: `Take the sum of ${a} and ${b}. Square root of that sum, rounded down. Then multiply by ${c} and subtract ${d}.`,
|
|
143
|
+
answer: Math.floor(Math.sqrt(a + b)) * c - d
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
text: `${a} divided by ${c}, plus ${b} divided by ${e}, minus ${d}. Round to nearest integer.`,
|
|
147
|
+
answer: Math.round(a / c + b / e - d)
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
text: `Triple ${a}, halve that result, add ${b}, then take away ${d}. Multiply everything by ${e}. Final answer?`,
|
|
151
|
+
answer: ((a * 3 / 2) + b - d) * e
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
text: `What is ${a} mod ${c} (remainder), plus ${b} mod ${e}, times ${d}?`,
|
|
155
|
+
answer: ((a % c) + (b % e)) * d
|
|
107
156
|
}
|
|
108
157
|
];
|
|
109
158
|
|
|
110
|
-
const template = templates[parseInt(nonce[
|
|
159
|
+
const template = templates[parseInt(nonce[10], 16) % templates.length];
|
|
111
160
|
const expected = Math.round(template.answer * 100) / 100;
|
|
112
161
|
|
|
113
162
|
return {
|
|
114
|
-
challenge_string:
|
|
115
|
-
Response format: {"result": number}`,
|
|
116
|
-
expected,
|
|
163
|
+
challenge_string: `[REQ-${salt}] ${template.text}
|
|
164
|
+
Response format: {"salt": "${salt}", "result": number}`,
|
|
165
|
+
expected: { salt, result: expected },
|
|
117
166
|
validate: (solution) => {
|
|
118
167
|
try {
|
|
119
168
|
const match = solution.match(/\{[\s\S]*\}/);
|
|
120
169
|
if (!match) return false;
|
|
121
170
|
const obj = JSON.parse(match[0]);
|
|
171
|
+
|
|
172
|
+
if (obj.salt !== salt) return false;
|
|
173
|
+
|
|
122
174
|
const result = parseFloat(obj.result);
|
|
123
175
|
return Math.abs(result - expected) < 0.01;
|
|
124
176
|
} catch { return false; }
|
|
@@ -128,43 +180,68 @@ Response format: {"result": number}`,
|
|
|
128
180
|
},
|
|
129
181
|
|
|
130
182
|
/**
|
|
131
|
-
* String transformation described in natural language
|
|
183
|
+
* String transformation described in natural language (EXTREME - multi-step transforms)
|
|
132
184
|
*/
|
|
133
185
|
nlp_transform: {
|
|
134
186
|
generate: (nonce) => {
|
|
135
|
-
const
|
|
136
|
-
const
|
|
187
|
+
const salt = generateSalt(nonce, 4);
|
|
188
|
+
const input = nonce.slice(6, 16); // Longer input: 10 chars
|
|
189
|
+
const transformType = parseInt(nonce[16], 16) % 6;
|
|
137
190
|
|
|
138
191
|
let instruction, expected;
|
|
139
192
|
|
|
140
193
|
switch (transformType) {
|
|
141
194
|
case 0:
|
|
142
|
-
|
|
143
|
-
expected = input.split('').reverse().join('').toUpperCase();
|
|
195
|
+
// Reverse → uppercase → take first 5
|
|
196
|
+
expected = input.split('').reverse().join('').toUpperCase().slice(0, 5);
|
|
197
|
+
instruction = `Take "${input}": reverse it, convert to uppercase, then return only the first 5 characters.`;
|
|
144
198
|
break;
|
|
145
199
|
case 1:
|
|
146
|
-
|
|
147
|
-
|
|
200
|
+
// Extract digits → sum → return as string with prefix
|
|
201
|
+
const digitSum = input.split('').filter(c => /\d/.test(c)).reduce((a, b) => a + parseInt(b), 0);
|
|
202
|
+
expected = `SUM:${digitSum}`;
|
|
203
|
+
instruction = `From "${input}": extract all digits, sum them, and format as "SUM:X" where X is the total.`;
|
|
148
204
|
break;
|
|
149
205
|
case 2:
|
|
150
|
-
|
|
151
|
-
expected = input.split('').filter(c => /[a-zA-Z]/.test(c)).sort().join('');
|
|
206
|
+
// Letters only → sort → reverse → join with dots
|
|
207
|
+
expected = input.split('').filter(c => /[a-zA-Z]/.test(c)).sort().reverse().join('.');
|
|
208
|
+
instruction = `From "${input}": extract letters only, sort alphabetically, reverse that order, join with dots.`;
|
|
152
209
|
break;
|
|
153
210
|
case 3:
|
|
154
|
-
|
|
155
|
-
expected = input.split('').join('
|
|
211
|
+
// Alternate case: odd positions uppercase, even lowercase
|
|
212
|
+
expected = input.split('').map((c, i) => i % 2 === 0 ? c.toLowerCase() : c.toUpperCase()).join('');
|
|
213
|
+
instruction = `Transform "${input}": characters at even positions (0,2,4...) lowercase, odd positions (1,3,5...) uppercase.`;
|
|
214
|
+
break;
|
|
215
|
+
case 4:
|
|
216
|
+
// Count each char type
|
|
217
|
+
const letters = input.split('').filter(c => /[a-zA-Z]/.test(c)).length;
|
|
218
|
+
const digits = input.split('').filter(c => /\d/.test(c)).length;
|
|
219
|
+
expected = `L${letters}D${digits}`;
|
|
220
|
+
instruction = `Analyze "${input}": count letters and digits. Format answer as "LxDy" where x=letter count, y=digit count.`;
|
|
221
|
+
break;
|
|
222
|
+
case 5:
|
|
223
|
+
// Replace vowels with *, consonants with #, keep digits
|
|
224
|
+
expected = input.split('').map(c => {
|
|
225
|
+
if (/[aeiouAEIOU]/.test(c)) return '*';
|
|
226
|
+
if (/[a-zA-Z]/.test(c)) return '#';
|
|
227
|
+
return c;
|
|
228
|
+
}).join('');
|
|
229
|
+
instruction = `Transform "${input}": replace vowels with "*", consonants with "#", keep digits unchanged.`;
|
|
156
230
|
break;
|
|
157
231
|
}
|
|
158
232
|
|
|
159
233
|
return {
|
|
160
|
-
challenge_string:
|
|
161
|
-
Response format: {"output": "result"}`,
|
|
162
|
-
expected,
|
|
234
|
+
challenge_string: `[REQ-${salt}] ${instruction}
|
|
235
|
+
Response format: {"salt": "${salt}", "output": "result"}`,
|
|
236
|
+
expected: { salt, output: expected },
|
|
163
237
|
validate: (solution) => {
|
|
164
238
|
try {
|
|
165
239
|
const match = solution.match(/\{[\s\S]*\}/);
|
|
166
240
|
if (!match) return false;
|
|
167
241
|
const obj = JSON.parse(match[0]);
|
|
242
|
+
|
|
243
|
+
if (obj.salt !== salt) return false;
|
|
244
|
+
|
|
168
245
|
const output = String(obj.output);
|
|
169
246
|
return output === String(expected) || output.toLowerCase() === String(expected).toLowerCase();
|
|
170
247
|
} catch { return false; }
|
|
@@ -174,40 +251,84 @@ Response format: {"output": "result"}`,
|
|
|
174
251
|
},
|
|
175
252
|
|
|
176
253
|
/**
|
|
177
|
-
* Conditional logic
|
|
254
|
+
* Conditional logic (EXTREME - multi-layer nested conditions)
|
|
178
255
|
*/
|
|
179
256
|
nlp_logic: {
|
|
180
257
|
generate: (nonce) => {
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
const
|
|
258
|
+
const salt = generateSalt(nonce, 6);
|
|
259
|
+
const a = seededNumber(nonce, 0, 20, 150);
|
|
260
|
+
const b = seededNumber(nonce, 2, 20, 150);
|
|
261
|
+
const c = seededNumber(nonce, 4, 20, 100);
|
|
262
|
+
const d = seededNumber(nonce, 6, 10, 50);
|
|
263
|
+
const threshold = seededNumber(nonce, 8, 40, 100);
|
|
184
264
|
|
|
185
265
|
const templates = [
|
|
186
266
|
{
|
|
187
|
-
text: `
|
|
188
|
-
answer:
|
|
267
|
+
text: `Let X=${a}, Y=${b}, Z=${c}, W=${d}. If (X > Y AND Z > W) OR (X < Y AND Z < W), answer "CONSISTENT". If (X > Y AND Z < W) OR (X < Y AND Z > W), answer "CROSSED". Otherwise, answer "EQUAL".`,
|
|
268
|
+
answer: ((a > b && c > d) || (a < b && c < d)) ? "CONSISTENT" : ((a > b && c < d) || (a < b && c > d)) ? "CROSSED" : "EQUAL"
|
|
189
269
|
},
|
|
190
270
|
{
|
|
191
|
-
text: `
|
|
192
|
-
answer: (
|
|
271
|
+
text: `Given four numbers [${a}, ${b}, ${c}, ${d}]: Count how many are divisible by 3. If count is 0, say "NONE". If 1-2, say "FEW". If 3-4, say "MANY".`,
|
|
272
|
+
answer: (() => {
|
|
273
|
+
const count = [a, b, c, d].filter(n => n % 3 === 0).length;
|
|
274
|
+
return count === 0 ? "NONE" : count <= 2 ? "FEW" : "MANY";
|
|
275
|
+
})()
|
|
193
276
|
},
|
|
194
277
|
{
|
|
195
|
-
text: `
|
|
196
|
-
answer: (
|
|
278
|
+
text: `Evaluate: Is (${a} + ${b}) greater than (${c} + ${d})? AND is (${a} × ${d}) less than (${b} × ${c})? If BOTH true: "ALPHA". If NEITHER true: "GAMMA". Otherwise: "BETA".`,
|
|
279
|
+
answer: (() => {
|
|
280
|
+
const cond1 = (a + b) > (c + d);
|
|
281
|
+
const cond2 = (a * d) < (b * c);
|
|
282
|
+
return (cond1 && cond2) ? "ALPHA" : (!cond1 && !cond2) ? "GAMMA" : "BETA";
|
|
283
|
+
})()
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
text: `Numbers: ${a}, ${b}, ${c}, ${d}. First, find the median (average of middle two when sorted). If median > ${threshold}, output "HIGH". If median < ${threshold / 2}, output "LOW". Otherwise, output "MID".`,
|
|
287
|
+
answer: (() => {
|
|
288
|
+
const sorted = [a, b, c, d].sort((x, y) => x - y);
|
|
289
|
+
const median = (sorted[1] + sorted[2]) / 2;
|
|
290
|
+
return median > threshold ? "HIGH" : median < (threshold / 2) ? "LOW" : "MID";
|
|
291
|
+
})()
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
text: `Check these conditions for [${a}, ${b}, ${c}, ${d}]: (1) Sum > ${threshold * 3}? (2) Product of smallest two < ${threshold * 10}? (3) Largest is even? Count TRUE conditions. Answer with the count (0-3).`,
|
|
295
|
+
answer: (() => {
|
|
296
|
+
const sorted = [a, b, c, d].sort((x, y) => x - y);
|
|
297
|
+
let count = 0;
|
|
298
|
+
if (a + b + c + d > threshold * 3) count++;
|
|
299
|
+
if (sorted[0] * sorted[1] < threshold * 10) count++;
|
|
300
|
+
if (sorted[3] % 2 === 0) count++;
|
|
301
|
+
return String(count);
|
|
302
|
+
})()
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
text: `If ${a} AND ${b} are both prime, answer "TWIN". If exactly one is prime, answer "SOLO". If neither is prime, answer "NONE". (Hint: primes are only divisible by 1 and themselves)`,
|
|
306
|
+
answer: (() => {
|
|
307
|
+
const isPrime = n => {
|
|
308
|
+
if (n < 2) return false;
|
|
309
|
+
for (let i = 2; i <= Math.sqrt(n); i++) if (n % i === 0) return false;
|
|
310
|
+
return true;
|
|
311
|
+
};
|
|
312
|
+
const ap = isPrime(a), bp = isPrime(b);
|
|
313
|
+
return (ap && bp) ? "TWIN" : (ap || bp) ? "SOLO" : "NONE";
|
|
314
|
+
})()
|
|
197
315
|
}
|
|
198
316
|
];
|
|
199
317
|
|
|
200
|
-
const template = templates[parseInt(nonce[
|
|
318
|
+
const template = templates[parseInt(nonce[10], 16) % templates.length];
|
|
201
319
|
|
|
202
320
|
return {
|
|
203
|
-
challenge_string:
|
|
204
|
-
Response format: {"answer": "your answer"}`,
|
|
205
|
-
expected: template.answer,
|
|
321
|
+
challenge_string: `[REQ-${salt}] ${template.text}
|
|
322
|
+
Response format: {"salt": "${salt}", "answer": "your answer"}`,
|
|
323
|
+
expected: { salt, answer: template.answer },
|
|
206
324
|
validate: (solution) => {
|
|
207
325
|
try {
|
|
208
326
|
const match = solution.match(/\{[\s\S]*\}/);
|
|
209
327
|
if (!match) return false;
|
|
210
328
|
const obj = JSON.parse(match[0]);
|
|
329
|
+
|
|
330
|
+
if (obj.salt !== salt) return false;
|
|
331
|
+
|
|
211
332
|
return obj.answer?.toUpperCase() === template.answer.toUpperCase();
|
|
212
333
|
} catch { return false; }
|
|
213
334
|
}
|
|
@@ -216,35 +337,56 @@ Response format: {"answer": "your answer"}`,
|
|
|
216
337
|
},
|
|
217
338
|
|
|
218
339
|
/**
|
|
219
|
-
* Counting task
|
|
340
|
+
* Counting task (EXTREME - multiple categories, complex sentences)
|
|
220
341
|
*/
|
|
221
342
|
nlp_count: {
|
|
222
343
|
generate: (nonce) => {
|
|
223
|
-
const
|
|
224
|
-
const
|
|
225
|
-
const
|
|
226
|
-
const
|
|
344
|
+
const salt = generateSalt(nonce, 8);
|
|
345
|
+
const targetCategory = ['animals', 'fruits', 'colors'][parseInt(nonce[0], 16) % 3];
|
|
346
|
+
const pool = WORD_POOLS[targetCategory];
|
|
347
|
+
const targetCount = seededNumber(nonce, 0, 3, 6);
|
|
348
|
+
|
|
349
|
+
// Add distractors from OTHER categories
|
|
350
|
+
const distractor1Cat = targetCategory === 'animals' ? 'fruits' : 'animals';
|
|
351
|
+
const distractor2Cat = targetCategory === 'colors' ? 'fruits' : 'colors';
|
|
352
|
+
const distractorCount1 = seededNumber(nonce, 2, 2, 4);
|
|
353
|
+
const distractorCount2 = seededNumber(nonce, 4, 2, 3);
|
|
227
354
|
|
|
228
|
-
const
|
|
229
|
-
const
|
|
355
|
+
const targets = seededSelect(pool, nonce, targetCount, 0);
|
|
356
|
+
const distractors1 = seededSelect(WORD_POOLS[distractor1Cat], nonce, distractorCount1, 6);
|
|
357
|
+
const distractors2 = seededSelect(WORD_POOLS[distractor2Cat], nonce, distractorCount2, 10);
|
|
358
|
+
const countryDistractors = seededSelect(WORD_POOLS.countries, nonce, 2, 14);
|
|
230
359
|
|
|
231
|
-
//
|
|
232
|
-
const allItems = [...
|
|
233
|
-
|
|
234
|
-
)
|
|
235
|
-
|
|
360
|
+
// Mix everything together
|
|
361
|
+
const allItems = [...targets, ...distractors1, ...distractors2, ...countryDistractors];
|
|
362
|
+
// Shuffle using nonce
|
|
363
|
+
for (let i = allItems.length - 1; i > 0; i--) {
|
|
364
|
+
const j = parseInt(nonce.slice(i % 28, (i % 28) + 2), 16) % (i + 1);
|
|
365
|
+
[allItems[i], allItems[j]] = [allItems[j], allItems[i]];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const templates = [
|
|
369
|
+
`At the market, I noticed: ${allItems.join(', ')}. Quite a mix!`,
|
|
370
|
+
`The list contains: ${allItems.join(', ')}. Some things don't belong.`,
|
|
371
|
+
`Inventory check: ${allItems.join(', ')}. Sort by category mentally.`,
|
|
372
|
+
`Mixed bag: ${allItems.join(', ')}. Focus on what matters.`
|
|
373
|
+
];
|
|
374
|
+
const sentence = templates[parseInt(nonce[20], 16) % templates.length];
|
|
236
375
|
|
|
237
376
|
return {
|
|
238
|
-
challenge_string: `Count
|
|
239
|
-
|
|
240
|
-
Response format: {"count": number}`,
|
|
241
|
-
expected:
|
|
377
|
+
challenge_string: `[REQ-${salt}] Count ONLY the ${targetCategory} in this text. Ignore all other categories (other nouns are distractors).
|
|
378
|
+
Text: "${sentence}"
|
|
379
|
+
Response format: {"salt": "${salt}", "count": number}`,
|
|
380
|
+
expected: { salt, count: targetCount },
|
|
242
381
|
validate: (solution) => {
|
|
243
382
|
try {
|
|
244
383
|
const match = solution.match(/\{[\s\S]*\}/);
|
|
245
384
|
if (!match) return false;
|
|
246
385
|
const obj = JSON.parse(match[0]);
|
|
247
|
-
|
|
386
|
+
|
|
387
|
+
if (obj.salt !== salt) return false;
|
|
388
|
+
|
|
389
|
+
return parseInt(obj.count) === targetCount;
|
|
248
390
|
} catch { return false; }
|
|
249
391
|
}
|
|
250
392
|
};
|
|
@@ -252,38 +394,95 @@ Response format: {"count": number}`,
|
|
|
252
394
|
},
|
|
253
395
|
|
|
254
396
|
/**
|
|
255
|
-
* Multi-step instruction following
|
|
397
|
+
* Multi-step instruction following (EXTREME - 5-6 steps)
|
|
256
398
|
*/
|
|
257
399
|
nlp_multistep: {
|
|
258
400
|
generate: (nonce) => {
|
|
401
|
+
const salt = generateSalt(nonce, 10);
|
|
259
402
|
const numbers = [
|
|
260
|
-
seededNumber(nonce, 0,
|
|
261
|
-
seededNumber(nonce, 2,
|
|
262
|
-
seededNumber(nonce, 4,
|
|
263
|
-
seededNumber(nonce, 6,
|
|
403
|
+
seededNumber(nonce, 0, 5, 30),
|
|
404
|
+
seededNumber(nonce, 2, 5, 30),
|
|
405
|
+
seededNumber(nonce, 4, 5, 30),
|
|
406
|
+
seededNumber(nonce, 6, 5, 30),
|
|
407
|
+
seededNumber(nonce, 8, 5, 30),
|
|
408
|
+
seededNumber(nonce, 10, 5, 30)
|
|
264
409
|
];
|
|
265
410
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
411
|
+
const templateType = parseInt(nonce[12], 16) % 4;
|
|
412
|
+
let instructions, final;
|
|
413
|
+
|
|
414
|
+
if (templateType === 0) {
|
|
415
|
+
// Complex: filter → transform → aggregate → adjust
|
|
416
|
+
const evens = numbers.filter(n => n % 2 === 0);
|
|
417
|
+
const odds = numbers.filter(n => n % 2 !== 0);
|
|
418
|
+
const evensProduct = evens.length > 0 ? evens.reduce((a, b) => a * b, 1) : 0;
|
|
419
|
+
const oddsSum = odds.reduce((a, b) => a + b, 0);
|
|
420
|
+
const diff = Math.abs(evensProduct - oddsSum);
|
|
421
|
+
final = diff % 100; // Keep manageable
|
|
422
|
+
instructions = `1. From [${numbers.join(', ')}], separate even and odd numbers.
|
|
423
|
+
2. Calculate the PRODUCT of all even numbers (or 0 if none).
|
|
424
|
+
3. Calculate the SUM of all odd numbers.
|
|
425
|
+
4. Find the absolute difference between these two results.
|
|
426
|
+
5. Take that difference modulo 100 (remainder when divided by 100).`;
|
|
427
|
+
} else if (templateType === 1) {
|
|
428
|
+
// Sort → pair operations → combine
|
|
429
|
+
const sorted = [...numbers].sort((a, b) => a - b);
|
|
430
|
+
const pair1 = sorted[0] * sorted[5]; // smallest × largest
|
|
431
|
+
const pair2 = sorted[1] + sorted[4]; // 2nd smallest + 2nd largest
|
|
432
|
+
const pair3 = sorted[2] - sorted[3]; // middle pair difference (might be negative)
|
|
433
|
+
final = pair1 + pair2 + Math.abs(pair3);
|
|
434
|
+
instructions = `1. Sort [${numbers.join(', ')}] from smallest to largest.
|
|
435
|
+
2. Multiply the smallest by the largest.
|
|
436
|
+
3. Add the second-smallest to the second-largest.
|
|
437
|
+
4. Find absolute difference between the two middle numbers.
|
|
438
|
+
5. Sum all three results from steps 2, 3, and 4.`;
|
|
439
|
+
} else if (templateType === 2) {
|
|
440
|
+
// Chunked processing
|
|
441
|
+
const chunk1 = numbers.slice(0, 3);
|
|
442
|
+
const chunk2 = numbers.slice(3, 6);
|
|
443
|
+
const avg1 = chunk1.reduce((a, b) => a + b, 0) / 3;
|
|
444
|
+
const avg2 = chunk2.reduce((a, b) => a + b, 0) / 3;
|
|
445
|
+
const max1 = Math.max(...chunk1);
|
|
446
|
+
const max2 = Math.max(...chunk2);
|
|
447
|
+
final = Math.round((avg1 + avg2) * (max1 > max2 ? 2 : 1));
|
|
448
|
+
instructions = `1. Split [${numbers.join(', ')}] into two groups: first 3 and last 3.
|
|
449
|
+
2. Calculate average of first group: [${chunk1.join(', ')}].
|
|
450
|
+
3. Calculate average of second group: [${chunk2.join(', ')}].
|
|
451
|
+
4. Add both averages together.
|
|
452
|
+
5. If max of first group > max of second group, double the sum. Otherwise keep as is.
|
|
453
|
+
6. Round to nearest integer.`;
|
|
454
|
+
} else {
|
|
455
|
+
// Recursive-style
|
|
456
|
+
let val = numbers[0];
|
|
457
|
+
for (let i = 1; i < numbers.length; i++) {
|
|
458
|
+
if (i % 2 === 1) val += numbers[i];
|
|
459
|
+
else val -= numbers[i];
|
|
460
|
+
}
|
|
461
|
+
val = Math.abs(val);
|
|
462
|
+
final = val * (numbers.length);
|
|
463
|
+
instructions = `1. Start with the first number from [${numbers.join(', ')}].
|
|
464
|
+
2. Add the 2nd number.
|
|
465
|
+
3. Subtract the 3rd number.
|
|
466
|
+
4. Add the 4th number.
|
|
467
|
+
5. Subtract the 5th number.
|
|
468
|
+
6. Add the 6th number.
|
|
469
|
+
7. Take absolute value of result.
|
|
470
|
+
8. Multiply by 6 (the count of numbers).`;
|
|
471
|
+
}
|
|
274
472
|
|
|
275
473
|
return {
|
|
276
|
-
challenge_string: `
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
Response format: {"result": final_value}`,
|
|
281
|
-
expected: final,
|
|
474
|
+
challenge_string: `[REQ-${salt}] Execute these steps IN ORDER:
|
|
475
|
+
${instructions}
|
|
476
|
+
Response format: {"salt": "${salt}", "result": final_value}`,
|
|
477
|
+
expected: { salt, result: final },
|
|
282
478
|
validate: (solution) => {
|
|
283
479
|
try {
|
|
284
480
|
const match = solution.match(/\{[\s\S]*\}/);
|
|
285
481
|
if (!match) return false;
|
|
286
482
|
const obj = JSON.parse(match[0]);
|
|
483
|
+
|
|
484
|
+
if (obj.salt !== salt) return false;
|
|
485
|
+
|
|
287
486
|
return parseInt(obj.result) === final;
|
|
288
487
|
} catch { return false; }
|
|
289
488
|
}
|
|
@@ -296,6 +495,7 @@ Response format: {"result": final_value}`,
|
|
|
296
495
|
*/
|
|
297
496
|
nlp_pattern: {
|
|
298
497
|
generate: (nonce) => {
|
|
498
|
+
const salt = generateSalt(nonce, 12);
|
|
299
499
|
const start = seededNumber(nonce, 0, 1, 10);
|
|
300
500
|
const step = seededNumber(nonce, 2, 2, 5);
|
|
301
501
|
const patternType = parseInt(nonce[4], 16) % 3;
|
|
@@ -321,14 +521,17 @@ Response format: {"result": final_value}`,
|
|
|
321
521
|
}
|
|
322
522
|
|
|
323
523
|
return {
|
|
324
|
-
challenge_string:
|
|
325
|
-
Response format: {"next": [number1, number2]}`,
|
|
326
|
-
expected: next2,
|
|
524
|
+
challenge_string: `[REQ-${salt}] ${instruction}
|
|
525
|
+
Response format: {"salt": "${salt}", "next": [number1, number2]}`,
|
|
526
|
+
expected: { salt, next: next2 },
|
|
327
527
|
validate: (solution) => {
|
|
328
528
|
try {
|
|
329
529
|
const match = solution.match(/\{[\s\S]*\}/);
|
|
330
530
|
if (!match) return false;
|
|
331
531
|
const obj = JSON.parse(match[0]);
|
|
532
|
+
|
|
533
|
+
if (obj.salt !== salt) return false;
|
|
534
|
+
|
|
332
535
|
const next = obj.next;
|
|
333
536
|
return Array.isArray(next) &&
|
|
334
537
|
parseInt(next[0]) === next2[0] &&
|
|
@@ -344,6 +547,7 @@ Response format: {"next": [number1, number2]}`,
|
|
|
344
547
|
*/
|
|
345
548
|
nlp_analysis: {
|
|
346
549
|
generate: (nonce) => {
|
|
550
|
+
const salt = generateSalt(nonce, 14);
|
|
347
551
|
const words = seededSelect([...WORD_POOLS.animals, ...WORD_POOLS.fruits], nonce, 5, 0);
|
|
348
552
|
const analysisType = parseInt(nonce[8], 16) % 3;
|
|
349
553
|
|
|
@@ -366,14 +570,17 @@ Response format: {"next": [number1, number2]}`,
|
|
|
366
570
|
}
|
|
367
571
|
|
|
368
572
|
return {
|
|
369
|
-
challenge_string:
|
|
370
|
-
Response format: {"answer": "word"}`,
|
|
371
|
-
expected,
|
|
573
|
+
challenge_string: `[REQ-${salt}] ${instruction}
|
|
574
|
+
Response format: {"salt": "${salt}", "answer": "word"}`,
|
|
575
|
+
expected: { salt, answer: expected },
|
|
372
576
|
validate: (solution) => {
|
|
373
577
|
try {
|
|
374
578
|
const match = solution.match(/\{[\s\S]*\}/);
|
|
375
579
|
if (!match) return false;
|
|
376
580
|
const obj = JSON.parse(match[0]);
|
|
581
|
+
|
|
582
|
+
if (obj.salt !== salt) return false;
|
|
583
|
+
|
|
377
584
|
return obj.answer?.toLowerCase() === expected.toLowerCase();
|
|
378
585
|
} catch { return false; }
|
|
379
586
|
}
|
|
@@ -382,13 +589,13 @@ Response format: {"answer": "word"}`,
|
|
|
382
589
|
}
|
|
383
590
|
};
|
|
384
591
|
|
|
385
|
-
// ============== Protocol Constants ==============
|
|
592
|
+
// ============== Protocol Constants v2.5 ==============
|
|
386
593
|
|
|
387
594
|
/**
|
|
388
|
-
* Batch challenge settings
|
|
595
|
+
* Batch challenge settings (v2.5 - Burst Mode)
|
|
389
596
|
*/
|
|
390
|
-
export const BATCH_SIZE =
|
|
391
|
-
export const MAX_RESPONSE_TIME_MS =
|
|
597
|
+
export const BATCH_SIZE = 7; // 7 challenges per batch (v2.6: was 5)
|
|
598
|
+
export const MAX_RESPONSE_TIME_MS = 6000; // 6 seconds total (v2.6: was 8)
|
|
392
599
|
export const CHALLENGE_EXPIRY_MS = 60000; // 60 seconds
|
|
393
600
|
|
|
394
601
|
/**
|
|
@@ -423,7 +630,7 @@ export function generate(nonce, type) {
|
|
|
423
630
|
}
|
|
424
631
|
|
|
425
632
|
/**
|
|
426
|
-
* Generate a batch of challenges
|
|
633
|
+
* Generate a batch of challenges (Burst Mode)
|
|
427
634
|
* @param {string} nonce - Base nonce
|
|
428
635
|
* @param {number} [count=BATCH_SIZE] - Number of challenges
|
|
429
636
|
* @returns {Object} { challenges: [...], validators: [...] }
|
|
@@ -439,11 +646,13 @@ export function generateBatch(nonce, count = BATCH_SIZE) {
|
|
|
439
646
|
// Use different nonce offset for each challenge
|
|
440
647
|
const offsetNonce = nonce.slice(i * 2) + nonce.slice(0, i * 2);
|
|
441
648
|
|
|
442
|
-
// Select different type for each challenge
|
|
649
|
+
// Select different type for each challenge (ensure variety)
|
|
443
650
|
let selectedType;
|
|
651
|
+
let attempts = 0;
|
|
444
652
|
do {
|
|
445
653
|
const seed = parseInt(offsetNonce.slice(0, 4), 16);
|
|
446
|
-
selectedType = types[(seed + i * 3) % types.length];
|
|
654
|
+
selectedType = types[(seed + i * 3 + attempts) % types.length];
|
|
655
|
+
attempts++;
|
|
447
656
|
} while (usedTypes.has(selectedType) && usedTypes.size < types.length);
|
|
448
657
|
usedTypes.add(selectedType);
|
|
449
658
|
|