aap-agent-server 2.5.0 → 2.7.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 +237 -77
- package/index.js +29 -3
- package/middleware.js +58 -13
- package/package.json +3 -2
- package/persistence.js +238 -0
- package/ratelimit.js +117 -0
- package/websocket.js +282 -0
- package/whitelist.js +231 -0
package/challenges.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @aap/server - Challenge Generator v2.
|
|
2
|
+
* @aap/server - Challenge Generator v2.6
|
|
3
3
|
*
|
|
4
4
|
* "Burst Mode with Entropy Injection"
|
|
5
|
-
* -
|
|
5
|
+
* - 7 challenges in 6 seconds (humans cannot pass)
|
|
6
6
|
* - Salt injection prevents caching attacks
|
|
7
7
|
* - Natural language instructions (requires LLM)
|
|
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
|
+
*
|
|
9
14
|
* v2.5 Changes:
|
|
10
15
|
* - BATCH_SIZE: 3 → 5
|
|
11
16
|
* - MAX_RESPONSE_TIME_MS: 12000 → 8000
|
|
@@ -68,23 +73,32 @@ function generateSalt(nonce, offset = 0) {
|
|
|
68
73
|
*/
|
|
69
74
|
export const CHALLENGE_TYPES = {
|
|
70
75
|
/**
|
|
71
|
-
* Extract entities from natural language sentence
|
|
76
|
+
* Extract entities from natural language sentence (HARD - more distractors)
|
|
72
77
|
*/
|
|
73
78
|
nlp_extract: {
|
|
74
79
|
generate: (nonce) => {
|
|
75
80
|
const salt = generateSalt(nonce, 0);
|
|
76
81
|
const category = ['animals', 'fruits', 'colors'][parseInt(nonce[0], 16) % 3];
|
|
77
82
|
const pool = WORD_POOLS[category];
|
|
78
|
-
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);
|
|
79
86
|
const verb = seededSelect(WORD_POOLS.verbs, nonce, 1, 4)[0];
|
|
87
|
+
const adj = seededSelect(WORD_POOLS.adjectives, nonce, 1, 6)[0];
|
|
80
88
|
|
|
81
|
-
|
|
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];
|
|
82
96
|
|
|
83
97
|
return {
|
|
84
|
-
challenge_string: `[REQ-${salt}] Extract
|
|
98
|
+
challenge_string: `[REQ-${salt}] Extract ONLY the ${category} from this sentence (ignore other categories).
|
|
85
99
|
Sentence: "${sentence}"
|
|
86
|
-
Response format: {"salt": "${salt}", "items": ["item1", "item2"]}`,
|
|
87
|
-
expected: { salt, items: targets.sort() },
|
|
100
|
+
Response format: {"salt": "${salt}", "items": ["item1", "item2", "item3"]}`,
|
|
101
|
+
expected: { salt, items: targets.map(s => s.toLowerCase()).sort() },
|
|
88
102
|
validate: (solution) => {
|
|
89
103
|
try {
|
|
90
104
|
const match = solution.match(/\{[\s\S]*\}/);
|
|
@@ -95,7 +109,8 @@ Response format: {"salt": "${salt}", "items": ["item1", "item2"]}`,
|
|
|
95
109
|
if (obj.salt !== salt) return false;
|
|
96
110
|
|
|
97
111
|
const items = (obj.items || obj.animals || obj.fruits || obj.colors || []).map(s => s.toLowerCase()).sort();
|
|
98
|
-
|
|
112
|
+
const expected = targets.map(s => s.toLowerCase()).sort();
|
|
113
|
+
return JSON.stringify(items) === JSON.stringify(expected);
|
|
99
114
|
} catch { return false; }
|
|
100
115
|
}
|
|
101
116
|
};
|
|
@@ -103,31 +118,45 @@ Response format: {"salt": "${salt}", "items": ["item1", "item2"]}`,
|
|
|
103
118
|
},
|
|
104
119
|
|
|
105
120
|
/**
|
|
106
|
-
* Math problem expressed in natural language
|
|
121
|
+
* Math problem expressed in natural language (EXTREME)
|
|
107
122
|
*/
|
|
108
123
|
nlp_math: {
|
|
109
124
|
generate: (nonce) => {
|
|
110
125
|
const salt = generateSalt(nonce, 2);
|
|
111
|
-
const a = seededNumber(nonce, 0,
|
|
112
|
-
const b = seededNumber(nonce, 2,
|
|
113
|
-
const c = seededNumber(nonce, 4, 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);
|
|
114
131
|
|
|
115
132
|
const templates = [
|
|
116
133
|
{
|
|
117
|
-
text: `Subtract ${b}
|
|
118
|
-
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
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
text: `Compute: ((${a} + ${b}) × ${c} - ${d}) ÷ ${e}. Give the result rounded to two decimal places.`,
|
|
139
|
+
answer: ((a + b) * c - d) / e
|
|
140
|
+
},
|
|
141
|
+
{
|
|
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
|
|
119
144
|
},
|
|
120
145
|
{
|
|
121
|
-
text:
|
|
122
|
-
answer: (a + b
|
|
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)
|
|
123
148
|
},
|
|
124
149
|
{
|
|
125
|
-
text: `
|
|
126
|
-
answer: a /
|
|
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
|
|
127
156
|
}
|
|
128
157
|
];
|
|
129
158
|
|
|
130
|
-
const template = templates[parseInt(nonce[
|
|
159
|
+
const template = templates[parseInt(nonce[10], 16) % templates.length];
|
|
131
160
|
const expected = Math.round(template.answer * 100) / 100;
|
|
132
161
|
|
|
133
162
|
return {
|
|
@@ -151,32 +180,53 @@ Response format: {"salt": "${salt}", "result": number}`,
|
|
|
151
180
|
},
|
|
152
181
|
|
|
153
182
|
/**
|
|
154
|
-
* String transformation described in natural language
|
|
183
|
+
* String transformation described in natural language (EXTREME - multi-step transforms)
|
|
155
184
|
*/
|
|
156
185
|
nlp_transform: {
|
|
157
186
|
generate: (nonce) => {
|
|
158
187
|
const salt = generateSalt(nonce, 4);
|
|
159
|
-
const input = nonce.slice(
|
|
160
|
-
const transformType = parseInt(nonce[
|
|
188
|
+
const input = nonce.slice(6, 16); // Longer input: 10 chars
|
|
189
|
+
const transformType = parseInt(nonce[16], 16) % 6;
|
|
161
190
|
|
|
162
191
|
let instruction, expected;
|
|
163
192
|
|
|
164
193
|
switch (transformType) {
|
|
165
194
|
case 0:
|
|
166
|
-
|
|
167
|
-
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.`;
|
|
168
198
|
break;
|
|
169
199
|
case 1:
|
|
170
|
-
|
|
171
|
-
|
|
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.`;
|
|
172
204
|
break;
|
|
173
205
|
case 2:
|
|
174
|
-
|
|
175
|
-
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.`;
|
|
176
209
|
break;
|
|
177
210
|
case 3:
|
|
178
|
-
|
|
179
|
-
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.`;
|
|
180
230
|
break;
|
|
181
231
|
}
|
|
182
232
|
|
|
@@ -201,31 +251,71 @@ Response format: {"salt": "${salt}", "output": "result"}`,
|
|
|
201
251
|
},
|
|
202
252
|
|
|
203
253
|
/**
|
|
204
|
-
* Conditional logic
|
|
254
|
+
* Conditional logic (EXTREME - multi-layer nested conditions)
|
|
205
255
|
*/
|
|
206
256
|
nlp_logic: {
|
|
207
257
|
generate: (nonce) => {
|
|
208
258
|
const salt = generateSalt(nonce, 6);
|
|
209
|
-
const a = seededNumber(nonce, 0,
|
|
210
|
-
const b = seededNumber(nonce, 2,
|
|
211
|
-
const
|
|
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);
|
|
212
264
|
|
|
213
265
|
const templates = [
|
|
214
266
|
{
|
|
215
|
-
text: `
|
|
216
|
-
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"
|
|
269
|
+
},
|
|
270
|
+
{
|
|
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
|
+
})()
|
|
276
|
+
},
|
|
277
|
+
{
|
|
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
|
+
})()
|
|
217
292
|
},
|
|
218
293
|
{
|
|
219
|
-
text: `
|
|
220
|
-
answer: (
|
|
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
|
+
})()
|
|
221
303
|
},
|
|
222
304
|
{
|
|
223
|
-
text: `If ${a}
|
|
224
|
-
answer: (
|
|
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
|
+
})()
|
|
225
315
|
}
|
|
226
316
|
];
|
|
227
317
|
|
|
228
|
-
const template = templates[parseInt(nonce[
|
|
318
|
+
const template = templates[parseInt(nonce[10], 16) % templates.length];
|
|
229
319
|
|
|
230
320
|
return {
|
|
231
321
|
challenge_string: `[REQ-${salt}] ${template.text}
|
|
@@ -247,30 +337,47 @@ Response format: {"salt": "${salt}", "answer": "your answer"}`,
|
|
|
247
337
|
},
|
|
248
338
|
|
|
249
339
|
/**
|
|
250
|
-
* Counting task
|
|
340
|
+
* Counting task (EXTREME - multiple categories, complex sentences)
|
|
251
341
|
*/
|
|
252
342
|
nlp_count: {
|
|
253
343
|
generate: (nonce) => {
|
|
254
344
|
const salt = generateSalt(nonce, 8);
|
|
255
|
-
const
|
|
256
|
-
const pool = WORD_POOLS[
|
|
257
|
-
const
|
|
258
|
-
|
|
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);
|
|
354
|
+
|
|
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);
|
|
259
359
|
|
|
260
|
-
|
|
261
|
-
const
|
|
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
|
+
}
|
|
262
367
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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];
|
|
268
375
|
|
|
269
376
|
return {
|
|
270
|
-
challenge_string: `[REQ-${salt}] Count
|
|
271
|
-
|
|
377
|
+
challenge_string: `[REQ-${salt}] Count ONLY the ${targetCategory} in this text. Ignore all other categories (other nouns are distractors).
|
|
378
|
+
Text: "${sentence}"
|
|
272
379
|
Response format: {"salt": "${salt}", "count": number}`,
|
|
273
|
-
expected: { salt, count:
|
|
380
|
+
expected: { salt, count: targetCount },
|
|
274
381
|
validate: (solution) => {
|
|
275
382
|
try {
|
|
276
383
|
const match = solution.match(/\{[\s\S]*\}/);
|
|
@@ -279,7 +386,7 @@ Response format: {"salt": "${salt}", "count": number}`,
|
|
|
279
386
|
|
|
280
387
|
if (obj.salt !== salt) return false;
|
|
281
388
|
|
|
282
|
-
return parseInt(obj.count) ===
|
|
389
|
+
return parseInt(obj.count) === targetCount;
|
|
283
390
|
} catch { return false; }
|
|
284
391
|
}
|
|
285
392
|
};
|
|
@@ -287,32 +394,85 @@ Response format: {"salt": "${salt}", "count": number}`,
|
|
|
287
394
|
},
|
|
288
395
|
|
|
289
396
|
/**
|
|
290
|
-
* Multi-step instruction following
|
|
397
|
+
* Multi-step instruction following (EXTREME - 5-6 steps)
|
|
291
398
|
*/
|
|
292
399
|
nlp_multistep: {
|
|
293
400
|
generate: (nonce) => {
|
|
294
401
|
const salt = generateSalt(nonce, 10);
|
|
295
402
|
const numbers = [
|
|
296
|
-
seededNumber(nonce, 0,
|
|
297
|
-
seededNumber(nonce, 2,
|
|
298
|
-
seededNumber(nonce, 4,
|
|
299
|
-
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)
|
|
300
409
|
];
|
|
301
410
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
+
}
|
|
310
472
|
|
|
311
473
|
return {
|
|
312
|
-
challenge_string: `[REQ-${salt}]
|
|
313
|
-
|
|
314
|
-
2. Multiply the result by the smallest number.
|
|
315
|
-
3. Subtract the largest number from that result.
|
|
474
|
+
challenge_string: `[REQ-${salt}] Execute these steps IN ORDER:
|
|
475
|
+
${instructions}
|
|
316
476
|
Response format: {"salt": "${salt}", "result": final_value}`,
|
|
317
477
|
expected: { salt, result: final },
|
|
318
478
|
validate: (solution) => {
|
|
@@ -434,8 +594,8 @@ Response format: {"salt": "${salt}", "answer": "word"}`,
|
|
|
434
594
|
/**
|
|
435
595
|
* Batch challenge settings (v2.5 - Burst Mode)
|
|
436
596
|
*/
|
|
437
|
-
export const BATCH_SIZE =
|
|
438
|
-
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)
|
|
439
599
|
export const CHALLENGE_EXPIRY_MS = 60000; // 60 seconds
|
|
440
600
|
|
|
441
601
|
/**
|
package/index.js
CHANGED
|
@@ -1,20 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @aap/server
|
|
2
|
+
* @aap/server v2.7.0
|
|
3
3
|
*
|
|
4
4
|
* Server-side utilities for Agent Attestation Protocol.
|
|
5
|
-
*
|
|
5
|
+
* The Reverse Turing Test - CAPTCHAs block bots, AAP blocks humans.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
export * from './middleware.js';
|
|
9
9
|
export * from './challenges.js';
|
|
10
|
+
export * from './ratelimit.js';
|
|
11
|
+
export * from './whitelist.js';
|
|
12
|
+
export * from './persistence.js';
|
|
13
|
+
export * from './errors.js';
|
|
14
|
+
export * from './websocket.js';
|
|
15
|
+
export * as logger from './logger.js';
|
|
10
16
|
|
|
11
17
|
import { aapMiddleware, createRouter } from './middleware.js';
|
|
12
18
|
import challenges from './challenges.js';
|
|
19
|
+
import { createRateLimiter, createFailureLimiter } from './ratelimit.js';
|
|
20
|
+
import { createWhitelist, createKeyRotation } from './whitelist.js';
|
|
21
|
+
import { createStore, createMemoryStore, createFileStore, createRedisStore } from './persistence.js';
|
|
22
|
+
import { createAAPWebSocket } from './websocket.js';
|
|
13
23
|
|
|
14
24
|
export { challenges };
|
|
15
25
|
|
|
16
26
|
export default {
|
|
27
|
+
// Core
|
|
17
28
|
aapMiddleware,
|
|
18
29
|
createRouter,
|
|
19
|
-
challenges
|
|
30
|
+
challenges,
|
|
31
|
+
|
|
32
|
+
// WebSocket (v2.7+)
|
|
33
|
+
createAAPWebSocket,
|
|
34
|
+
|
|
35
|
+
// Security
|
|
36
|
+
createRateLimiter,
|
|
37
|
+
createFailureLimiter,
|
|
38
|
+
createWhitelist,
|
|
39
|
+
createKeyRotation,
|
|
40
|
+
|
|
41
|
+
// Persistence
|
|
42
|
+
createStore,
|
|
43
|
+
createMemoryStore,
|
|
44
|
+
createFileStore,
|
|
45
|
+
createRedisStore
|
|
20
46
|
};
|
package/middleware.js
CHANGED
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
MAX_RESPONSE_TIME_MS,
|
|
16
16
|
CHALLENGE_EXPIRY_MS
|
|
17
17
|
} from './challenges.js';
|
|
18
|
+
import * as logger from './logger.js';
|
|
19
|
+
import { ErrorCodes, sendError } from './errors.js';
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* Create AAP verification middleware/router
|
|
@@ -38,7 +40,8 @@ export function aapMiddleware(options = {}) {
|
|
|
38
40
|
onFailed
|
|
39
41
|
} = options;
|
|
40
42
|
|
|
41
|
-
// In-memory challenge store
|
|
43
|
+
// In-memory challenge store with size limit (DoS protection)
|
|
44
|
+
const MAX_CHALLENGES = 10000;
|
|
42
45
|
const challenges = new Map();
|
|
43
46
|
|
|
44
47
|
// Cleanup expired challenges periodically
|
|
@@ -49,6 +52,15 @@ export function aapMiddleware(options = {}) {
|
|
|
49
52
|
challenges.delete(nonce);
|
|
50
53
|
}
|
|
51
54
|
}
|
|
55
|
+
|
|
56
|
+
// Emergency cleanup if still too many (keep newest)
|
|
57
|
+
if (challenges.size > MAX_CHALLENGES) {
|
|
58
|
+
const entries = [...challenges.entries()]
|
|
59
|
+
.sort((a, b) => b[1].timestamp - a[1].timestamp)
|
|
60
|
+
.slice(0, MAX_CHALLENGES / 2);
|
|
61
|
+
challenges.clear();
|
|
62
|
+
entries.forEach(([k, v]) => challenges.set(k, v));
|
|
63
|
+
}
|
|
52
64
|
};
|
|
53
65
|
|
|
54
66
|
// Return a function that creates routes
|
|
@@ -121,6 +133,7 @@ export function aapMiddleware(options = {}) {
|
|
|
121
133
|
} = req.body;
|
|
122
134
|
|
|
123
135
|
const checks = {
|
|
136
|
+
inputValid: false,
|
|
124
137
|
challengeExists: false,
|
|
125
138
|
notExpired: false,
|
|
126
139
|
solutionsExist: false,
|
|
@@ -130,7 +143,28 @@ export function aapMiddleware(options = {}) {
|
|
|
130
143
|
};
|
|
131
144
|
|
|
132
145
|
try {
|
|
133
|
-
// Check
|
|
146
|
+
// Check 0: Input validation (security)
|
|
147
|
+
if (!nonce || typeof nonce !== 'string' || nonce.length !== 32) {
|
|
148
|
+
return res.status(400).json({ verified: false, error: 'Invalid nonce format', checks });
|
|
149
|
+
}
|
|
150
|
+
if (!publicId || typeof publicId !== 'string' || publicId.length !== 20) {
|
|
151
|
+
return res.status(400).json({ verified: false, error: 'Invalid publicId format', checks });
|
|
152
|
+
}
|
|
153
|
+
if (!signature || typeof signature !== 'string' || signature.length < 50) {
|
|
154
|
+
return res.status(400).json({ verified: false, error: 'Invalid signature format', checks });
|
|
155
|
+
}
|
|
156
|
+
if (!publicKey || typeof publicKey !== 'string' || !publicKey.includes('BEGIN PUBLIC KEY')) {
|
|
157
|
+
return res.status(400).json({ verified: false, error: 'Invalid publicKey format', checks });
|
|
158
|
+
}
|
|
159
|
+
if (!timestamp || typeof timestamp !== 'number') {
|
|
160
|
+
return res.status(400).json({ verified: false, error: 'Invalid timestamp', checks });
|
|
161
|
+
}
|
|
162
|
+
if (!responseTimeMs || typeof responseTimeMs !== 'number' || responseTimeMs < 0) {
|
|
163
|
+
return res.status(400).json({ verified: false, error: 'Invalid responseTimeMs', checks });
|
|
164
|
+
}
|
|
165
|
+
checks.inputValid = true;
|
|
166
|
+
|
|
167
|
+
// Check 1: Challenge exists (check BEFORE delete for race condition fix)
|
|
134
168
|
const challenge = challenges.get(nonce);
|
|
135
169
|
if (!challenge) {
|
|
136
170
|
if (onFailed) onFailed({ error: 'Challenge not found', checks }, req);
|
|
@@ -142,12 +176,9 @@ export function aapMiddleware(options = {}) {
|
|
|
142
176
|
}
|
|
143
177
|
checks.challengeExists = true;
|
|
144
178
|
|
|
145
|
-
//
|
|
146
|
-
const { validators, batchSize: size } = challenge;
|
|
147
|
-
challenges.delete(nonce);
|
|
148
|
-
|
|
149
|
-
// Check 2: Not expired
|
|
179
|
+
// Check 2: Not expired (check BEFORE delete - race condition fix)
|
|
150
180
|
if (Date.now() > challenge.expiresAt) {
|
|
181
|
+
challenges.delete(nonce); // Clean up expired
|
|
151
182
|
if (onFailed) onFailed({ error: 'Challenge expired', checks }, req);
|
|
152
183
|
return res.status(400).json({
|
|
153
184
|
verified: false,
|
|
@@ -157,6 +188,10 @@ export function aapMiddleware(options = {}) {
|
|
|
157
188
|
}
|
|
158
189
|
checks.notExpired = true;
|
|
159
190
|
|
|
191
|
+
// Remove challenge (one-time use) - only after expiry check
|
|
192
|
+
const { validators, batchSize: size } = challenge;
|
|
193
|
+
challenges.delete(nonce);
|
|
194
|
+
|
|
160
195
|
// Check 3: Solutions exist
|
|
161
196
|
if (!solutions || !Array.isArray(solutions) || solutions.length !== size) {
|
|
162
197
|
if (onFailed) onFailed({ error: 'Invalid solutions array', checks }, req);
|
|
@@ -183,13 +218,17 @@ export function aapMiddleware(options = {}) {
|
|
|
183
218
|
}
|
|
184
219
|
checks.solutionsValid = true;
|
|
185
220
|
|
|
186
|
-
// Check 5: Response time (Proof of Liveness)
|
|
187
|
-
|
|
221
|
+
// Check 5: Response time (Proof of Liveness) - SERVER-SIDE validation
|
|
222
|
+
const serverResponseTime = Date.now() - challenge.timestamp;
|
|
223
|
+
const effectiveResponseTime = Math.max(responseTimeMs, serverResponseTime);
|
|
224
|
+
|
|
225
|
+
if (effectiveResponseTime > maxResponseTimeMs) {
|
|
188
226
|
if (onFailed) onFailed({ error: 'Response too slow', checks }, req);
|
|
189
227
|
return res.status(400).json({
|
|
190
228
|
verified: false,
|
|
191
|
-
error: `Response too slow: ${
|
|
192
|
-
checks
|
|
229
|
+
error: `Response too slow: ${effectiveResponseTime}ms > ${maxResponseTimeMs}ms (Proof of Liveness failed)`,
|
|
230
|
+
checks,
|
|
231
|
+
timing: { client: responseTimeMs, server: serverResponseTime }
|
|
193
232
|
});
|
|
194
233
|
}
|
|
195
234
|
checks.responseTimeValid = true;
|
|
@@ -361,8 +400,14 @@ export function aapMiddleware(options = {}) {
|
|
|
361
400
|
* const app = express();
|
|
362
401
|
* app.use('/aap/v1', createRouter());
|
|
363
402
|
*/
|
|
364
|
-
export function createRouter(options = {}) {
|
|
365
|
-
|
|
403
|
+
export async function createRouter(options = {}) {
|
|
404
|
+
// Dynamic import for optional express dependency
|
|
405
|
+
let express;
|
|
406
|
+
try {
|
|
407
|
+
express = (await import('express')).default;
|
|
408
|
+
} catch {
|
|
409
|
+
throw new Error('express is required for createRouter. Install with: npm install express');
|
|
410
|
+
}
|
|
366
411
|
const router = express.Router();
|
|
367
412
|
router.use(express.json());
|
|
368
413
|
|