aap-agent-server 2.0.0 → 2.5.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 +92 -43
- package/middleware.js +2 -2
- package/package.json +2 -2
package/challenges.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @aap/server - Challenge Generator v2.
|
|
2
|
+
* @aap/server - Challenge Generator v2.5
|
|
3
3
|
*
|
|
4
|
-
* "
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
4
|
+
* "Burst Mode with Entropy Injection"
|
|
5
|
+
* - 5 challenges in 8 seconds (humans cannot pass)
|
|
6
|
+
* - Salt injection prevents caching attacks
|
|
7
|
+
* - Natural language instructions (requires LLM)
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
+
* v2.5 Changes:
|
|
10
|
+
* - BATCH_SIZE: 3 → 5
|
|
11
|
+
* - MAX_RESPONSE_TIME_MS: 12000 → 8000
|
|
12
|
+
* - Salt injection in challenges (must be echoed back)
|
|
9
13
|
*/
|
|
10
14
|
|
|
11
15
|
import { createHash } from 'node:crypto';
|
|
@@ -14,12 +18,12 @@ import { createHash } from 'node:crypto';
|
|
|
14
18
|
* Word pools for dynamic challenge generation
|
|
15
19
|
*/
|
|
16
20
|
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']
|
|
21
|
+
animals: ['cat', 'dog', 'rabbit', 'tiger', 'lion', 'elephant', 'giraffe', 'penguin', 'eagle', 'shark', 'wolf', 'bear', 'fox', 'deer', 'owl'],
|
|
22
|
+
fruits: ['apple', 'banana', 'orange', 'grape', 'strawberry', 'watermelon', 'peach', 'kiwi', 'mango', 'cherry', 'lemon', 'lime', 'pear', 'plum'],
|
|
23
|
+
colors: ['red', 'blue', 'yellow', 'green', 'purple', 'orange', 'pink', 'black', 'white', 'brown', 'gray', 'cyan', 'magenta'],
|
|
24
|
+
countries: ['Korea', 'Japan', 'USA', 'UK', 'France', 'Germany', 'Australia', 'Canada', 'Brazil', 'India', 'Italy', 'Spain', 'Mexico'],
|
|
25
|
+
verbs: ['runs', 'eats', 'sleeps', 'plays', 'works', 'studies', 'travels', 'cooks', 'reads', 'writes', 'sings', 'dances'],
|
|
26
|
+
adjectives: ['big', 'small', 'fast', 'slow', 'beautiful', 'cute', 'delicious', 'interesting', 'bright', 'dark']
|
|
23
27
|
};
|
|
24
28
|
|
|
25
29
|
/**
|
|
@@ -49,6 +53,16 @@ function seededNumber(nonce, offset, min, max) {
|
|
|
49
53
|
return (seed % (max - min + 1)) + min;
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Generate salt from nonce (for entropy injection)
|
|
58
|
+
* @param {string} nonce - Base nonce
|
|
59
|
+
* @param {number} offset - Offset for variation
|
|
60
|
+
* @returns {string} 6-character salt
|
|
61
|
+
*/
|
|
62
|
+
function generateSalt(nonce, offset = 0) {
|
|
63
|
+
return nonce.slice(offset, offset + 6).toUpperCase();
|
|
64
|
+
}
|
|
65
|
+
|
|
52
66
|
/**
|
|
53
67
|
* Challenge type definitions
|
|
54
68
|
*/
|
|
@@ -58,6 +72,7 @@ export const CHALLENGE_TYPES = {
|
|
|
58
72
|
*/
|
|
59
73
|
nlp_extract: {
|
|
60
74
|
generate: (nonce) => {
|
|
75
|
+
const salt = generateSalt(nonce, 0);
|
|
61
76
|
const category = ['animals', 'fruits', 'colors'][parseInt(nonce[0], 16) % 3];
|
|
62
77
|
const pool = WORD_POOLS[category];
|
|
63
78
|
const targets = seededSelect(pool, nonce, 2, 0);
|
|
@@ -66,15 +81,19 @@ export const CHALLENGE_TYPES = {
|
|
|
66
81
|
const sentence = `The ${targets[0]} and ${targets[1]} ${verb} in the park.`;
|
|
67
82
|
|
|
68
83
|
return {
|
|
69
|
-
challenge_string: `Extract only the ${category} from the following sentence
|
|
84
|
+
challenge_string: `[REQ-${salt}] Extract only the ${category} from the following sentence.
|
|
70
85
|
Sentence: "${sentence}"
|
|
71
|
-
Response format: {"items": ["item1", "item2"]}`,
|
|
72
|
-
expected: targets.sort(),
|
|
86
|
+
Response format: {"salt": "${salt}", "items": ["item1", "item2"]}`,
|
|
87
|
+
expected: { salt, items: targets.sort() },
|
|
73
88
|
validate: (solution) => {
|
|
74
89
|
try {
|
|
75
90
|
const match = solution.match(/\{[\s\S]*\}/);
|
|
76
91
|
if (!match) return false;
|
|
77
92
|
const obj = JSON.parse(match[0]);
|
|
93
|
+
|
|
94
|
+
// Check salt
|
|
95
|
+
if (obj.salt !== salt) return false;
|
|
96
|
+
|
|
78
97
|
const items = (obj.items || obj.animals || obj.fruits || obj.colors || []).map(s => s.toLowerCase()).sort();
|
|
79
98
|
return JSON.stringify(items) === JSON.stringify(targets.map(s => s.toLowerCase()).sort());
|
|
80
99
|
} catch { return false; }
|
|
@@ -88,6 +107,7 @@ Response format: {"items": ["item1", "item2"]}`,
|
|
|
88
107
|
*/
|
|
89
108
|
nlp_math: {
|
|
90
109
|
generate: (nonce) => {
|
|
110
|
+
const salt = generateSalt(nonce, 2);
|
|
91
111
|
const a = seededNumber(nonce, 0, 10, 50);
|
|
92
112
|
const b = seededNumber(nonce, 2, 5, 20);
|
|
93
113
|
const c = seededNumber(nonce, 4, 2, 5);
|
|
@@ -111,14 +131,17 @@ Response format: {"items": ["item1", "item2"]}`,
|
|
|
111
131
|
const expected = Math.round(template.answer * 100) / 100;
|
|
112
132
|
|
|
113
133
|
return {
|
|
114
|
-
challenge_string:
|
|
115
|
-
Response format: {"result": number}`,
|
|
116
|
-
expected,
|
|
134
|
+
challenge_string: `[REQ-${salt}] ${template.text}
|
|
135
|
+
Response format: {"salt": "${salt}", "result": number}`,
|
|
136
|
+
expected: { salt, result: expected },
|
|
117
137
|
validate: (solution) => {
|
|
118
138
|
try {
|
|
119
139
|
const match = solution.match(/\{[\s\S]*\}/);
|
|
120
140
|
if (!match) return false;
|
|
121
141
|
const obj = JSON.parse(match[0]);
|
|
142
|
+
|
|
143
|
+
if (obj.salt !== salt) return false;
|
|
144
|
+
|
|
122
145
|
const result = parseFloat(obj.result);
|
|
123
146
|
return Math.abs(result - expected) < 0.01;
|
|
124
147
|
} catch { return false; }
|
|
@@ -132,7 +155,8 @@ Response format: {"result": number}`,
|
|
|
132
155
|
*/
|
|
133
156
|
nlp_transform: {
|
|
134
157
|
generate: (nonce) => {
|
|
135
|
-
const
|
|
158
|
+
const salt = generateSalt(nonce, 4);
|
|
159
|
+
const input = nonce.slice(8, 14);
|
|
136
160
|
const transformType = parseInt(nonce[6], 16) % 4;
|
|
137
161
|
|
|
138
162
|
let instruction, expected;
|
|
@@ -157,14 +181,17 @@ Response format: {"result": number}`,
|
|
|
157
181
|
}
|
|
158
182
|
|
|
159
183
|
return {
|
|
160
|
-
challenge_string:
|
|
161
|
-
Response format: {"output": "result"}`,
|
|
162
|
-
expected,
|
|
184
|
+
challenge_string: `[REQ-${salt}] ${instruction}
|
|
185
|
+
Response format: {"salt": "${salt}", "output": "result"}`,
|
|
186
|
+
expected: { salt, output: expected },
|
|
163
187
|
validate: (solution) => {
|
|
164
188
|
try {
|
|
165
189
|
const match = solution.match(/\{[\s\S]*\}/);
|
|
166
190
|
if (!match) return false;
|
|
167
191
|
const obj = JSON.parse(match[0]);
|
|
192
|
+
|
|
193
|
+
if (obj.salt !== salt) return false;
|
|
194
|
+
|
|
168
195
|
const output = String(obj.output);
|
|
169
196
|
return output === String(expected) || output.toLowerCase() === String(expected).toLowerCase();
|
|
170
197
|
} catch { return false; }
|
|
@@ -178,6 +205,7 @@ Response format: {"output": "result"}`,
|
|
|
178
205
|
*/
|
|
179
206
|
nlp_logic: {
|
|
180
207
|
generate: (nonce) => {
|
|
208
|
+
const salt = generateSalt(nonce, 6);
|
|
181
209
|
const a = seededNumber(nonce, 0, 10, 100);
|
|
182
210
|
const b = seededNumber(nonce, 2, 10, 100);
|
|
183
211
|
const threshold = seededNumber(nonce, 4, 20, 80);
|
|
@@ -200,14 +228,17 @@ Response format: {"output": "result"}`,
|
|
|
200
228
|
const template = templates[parseInt(nonce[6], 16) % templates.length];
|
|
201
229
|
|
|
202
230
|
return {
|
|
203
|
-
challenge_string:
|
|
204
|
-
Response format: {"answer": "your answer"}`,
|
|
205
|
-
expected: template.answer,
|
|
231
|
+
challenge_string: `[REQ-${salt}] ${template.text}
|
|
232
|
+
Response format: {"salt": "${salt}", "answer": "your answer"}`,
|
|
233
|
+
expected: { salt, answer: template.answer },
|
|
206
234
|
validate: (solution) => {
|
|
207
235
|
try {
|
|
208
236
|
const match = solution.match(/\{[\s\S]*\}/);
|
|
209
237
|
if (!match) return false;
|
|
210
238
|
const obj = JSON.parse(match[0]);
|
|
239
|
+
|
|
240
|
+
if (obj.salt !== salt) return false;
|
|
241
|
+
|
|
211
242
|
return obj.answer?.toUpperCase() === template.answer.toUpperCase();
|
|
212
243
|
} catch { return false; }
|
|
213
244
|
}
|
|
@@ -220,6 +251,7 @@ Response format: {"answer": "your answer"}`,
|
|
|
220
251
|
*/
|
|
221
252
|
nlp_count: {
|
|
222
253
|
generate: (nonce) => {
|
|
254
|
+
const salt = generateSalt(nonce, 8);
|
|
223
255
|
const category = ['animals', 'fruits', 'colors'][parseInt(nonce[0], 16) % 3];
|
|
224
256
|
const pool = WORD_POOLS[category];
|
|
225
257
|
const count1 = seededNumber(nonce, 0, 2, 4);
|
|
@@ -235,15 +267,18 @@ Response format: {"answer": "your answer"}`,
|
|
|
235
267
|
const sentence = `I see ${allItems.join(', ')} in the picture.`;
|
|
236
268
|
|
|
237
269
|
return {
|
|
238
|
-
challenge_string: `Count only the ${category} in the following sentence.
|
|
270
|
+
challenge_string: `[REQ-${salt}] Count only the ${category} in the following sentence.
|
|
239
271
|
Sentence: "${sentence}"
|
|
240
|
-
Response format: {"count": number}`,
|
|
241
|
-
expected: count1,
|
|
272
|
+
Response format: {"salt": "${salt}", "count": number}`,
|
|
273
|
+
expected: { salt, count: count1 },
|
|
242
274
|
validate: (solution) => {
|
|
243
275
|
try {
|
|
244
276
|
const match = solution.match(/\{[\s\S]*\}/);
|
|
245
277
|
if (!match) return false;
|
|
246
278
|
const obj = JSON.parse(match[0]);
|
|
279
|
+
|
|
280
|
+
if (obj.salt !== salt) return false;
|
|
281
|
+
|
|
247
282
|
return parseInt(obj.count) === count1;
|
|
248
283
|
} catch { return false; }
|
|
249
284
|
}
|
|
@@ -256,6 +291,7 @@ Response format: {"count": number}`,
|
|
|
256
291
|
*/
|
|
257
292
|
nlp_multistep: {
|
|
258
293
|
generate: (nonce) => {
|
|
294
|
+
const salt = generateSalt(nonce, 10);
|
|
259
295
|
const numbers = [
|
|
260
296
|
seededNumber(nonce, 0, 1, 9),
|
|
261
297
|
seededNumber(nonce, 2, 1, 9),
|
|
@@ -273,17 +309,20 @@ Response format: {"count": number}`,
|
|
|
273
309
|
const final = step2 - max;
|
|
274
310
|
|
|
275
311
|
return {
|
|
276
|
-
challenge_string: `Follow these instructions in order:
|
|
312
|
+
challenge_string: `[REQ-${salt}] Follow these instructions in order:
|
|
277
313
|
1. Add all the numbers in [${numbers.join(', ')}] together.
|
|
278
314
|
2. Multiply the result by the smallest number.
|
|
279
315
|
3. Subtract the largest number from that result.
|
|
280
|
-
Response format: {"result": final_value}`,
|
|
281
|
-
expected: final,
|
|
316
|
+
Response format: {"salt": "${salt}", "result": final_value}`,
|
|
317
|
+
expected: { salt, result: final },
|
|
282
318
|
validate: (solution) => {
|
|
283
319
|
try {
|
|
284
320
|
const match = solution.match(/\{[\s\S]*\}/);
|
|
285
321
|
if (!match) return false;
|
|
286
322
|
const obj = JSON.parse(match[0]);
|
|
323
|
+
|
|
324
|
+
if (obj.salt !== salt) return false;
|
|
325
|
+
|
|
287
326
|
return parseInt(obj.result) === final;
|
|
288
327
|
} catch { return false; }
|
|
289
328
|
}
|
|
@@ -296,6 +335,7 @@ Response format: {"result": final_value}`,
|
|
|
296
335
|
*/
|
|
297
336
|
nlp_pattern: {
|
|
298
337
|
generate: (nonce) => {
|
|
338
|
+
const salt = generateSalt(nonce, 12);
|
|
299
339
|
const start = seededNumber(nonce, 0, 1, 10);
|
|
300
340
|
const step = seededNumber(nonce, 2, 2, 5);
|
|
301
341
|
const patternType = parseInt(nonce[4], 16) % 3;
|
|
@@ -321,14 +361,17 @@ Response format: {"result": final_value}`,
|
|
|
321
361
|
}
|
|
322
362
|
|
|
323
363
|
return {
|
|
324
|
-
challenge_string:
|
|
325
|
-
Response format: {"next": [number1, number2]}`,
|
|
326
|
-
expected: next2,
|
|
364
|
+
challenge_string: `[REQ-${salt}] ${instruction}
|
|
365
|
+
Response format: {"salt": "${salt}", "next": [number1, number2]}`,
|
|
366
|
+
expected: { salt, next: next2 },
|
|
327
367
|
validate: (solution) => {
|
|
328
368
|
try {
|
|
329
369
|
const match = solution.match(/\{[\s\S]*\}/);
|
|
330
370
|
if (!match) return false;
|
|
331
371
|
const obj = JSON.parse(match[0]);
|
|
372
|
+
|
|
373
|
+
if (obj.salt !== salt) return false;
|
|
374
|
+
|
|
332
375
|
const next = obj.next;
|
|
333
376
|
return Array.isArray(next) &&
|
|
334
377
|
parseInt(next[0]) === next2[0] &&
|
|
@@ -344,6 +387,7 @@ Response format: {"next": [number1, number2]}`,
|
|
|
344
387
|
*/
|
|
345
388
|
nlp_analysis: {
|
|
346
389
|
generate: (nonce) => {
|
|
390
|
+
const salt = generateSalt(nonce, 14);
|
|
347
391
|
const words = seededSelect([...WORD_POOLS.animals, ...WORD_POOLS.fruits], nonce, 5, 0);
|
|
348
392
|
const analysisType = parseInt(nonce[8], 16) % 3;
|
|
349
393
|
|
|
@@ -366,14 +410,17 @@ Response format: {"next": [number1, number2]}`,
|
|
|
366
410
|
}
|
|
367
411
|
|
|
368
412
|
return {
|
|
369
|
-
challenge_string:
|
|
370
|
-
Response format: {"answer": "word"}`,
|
|
371
|
-
expected,
|
|
413
|
+
challenge_string: `[REQ-${salt}] ${instruction}
|
|
414
|
+
Response format: {"salt": "${salt}", "answer": "word"}`,
|
|
415
|
+
expected: { salt, answer: expected },
|
|
372
416
|
validate: (solution) => {
|
|
373
417
|
try {
|
|
374
418
|
const match = solution.match(/\{[\s\S]*\}/);
|
|
375
419
|
if (!match) return false;
|
|
376
420
|
const obj = JSON.parse(match[0]);
|
|
421
|
+
|
|
422
|
+
if (obj.salt !== salt) return false;
|
|
423
|
+
|
|
377
424
|
return obj.answer?.toLowerCase() === expected.toLowerCase();
|
|
378
425
|
} catch { return false; }
|
|
379
426
|
}
|
|
@@ -382,13 +429,13 @@ Response format: {"answer": "word"}`,
|
|
|
382
429
|
}
|
|
383
430
|
};
|
|
384
431
|
|
|
385
|
-
// ============== Protocol Constants ==============
|
|
432
|
+
// ============== Protocol Constants v2.5 ==============
|
|
386
433
|
|
|
387
434
|
/**
|
|
388
|
-
* Batch challenge settings
|
|
435
|
+
* Batch challenge settings (v2.5 - Burst Mode)
|
|
389
436
|
*/
|
|
390
|
-
export const BATCH_SIZE =
|
|
391
|
-
export const MAX_RESPONSE_TIME_MS =
|
|
437
|
+
export const BATCH_SIZE = 5; // 5 challenges per batch (was 3)
|
|
438
|
+
export const MAX_RESPONSE_TIME_MS = 8000; // 8 seconds total (was 12)
|
|
392
439
|
export const CHALLENGE_EXPIRY_MS = 60000; // 60 seconds
|
|
393
440
|
|
|
394
441
|
/**
|
|
@@ -423,7 +470,7 @@ export function generate(nonce, type) {
|
|
|
423
470
|
}
|
|
424
471
|
|
|
425
472
|
/**
|
|
426
|
-
* Generate a batch of challenges
|
|
473
|
+
* Generate a batch of challenges (Burst Mode)
|
|
427
474
|
* @param {string} nonce - Base nonce
|
|
428
475
|
* @param {number} [count=BATCH_SIZE] - Number of challenges
|
|
429
476
|
* @returns {Object} { challenges: [...], validators: [...] }
|
|
@@ -439,11 +486,13 @@ export function generateBatch(nonce, count = BATCH_SIZE) {
|
|
|
439
486
|
// Use different nonce offset for each challenge
|
|
440
487
|
const offsetNonce = nonce.slice(i * 2) + nonce.slice(0, i * 2);
|
|
441
488
|
|
|
442
|
-
// Select different type for each challenge
|
|
489
|
+
// Select different type for each challenge (ensure variety)
|
|
443
490
|
let selectedType;
|
|
491
|
+
let attempts = 0;
|
|
444
492
|
do {
|
|
445
493
|
const seed = parseInt(offsetNonce.slice(0, 4), 16);
|
|
446
|
-
selectedType = types[(seed + i * 3) % types.length];
|
|
494
|
+
selectedType = types[(seed + i * 3 + attempts) % types.length];
|
|
495
|
+
attempts++;
|
|
447
496
|
} while (usedTypes.has(selectedType) && usedTypes.size < types.length);
|
|
448
497
|
usedTypes.add(selectedType);
|
|
449
498
|
|
package/middleware.js
CHANGED
|
@@ -21,8 +21,8 @@ import {
|
|
|
21
21
|
*
|
|
22
22
|
* @param {Object} [options]
|
|
23
23
|
* @param {number} [options.challengeExpiryMs=60000] - Challenge expiration time
|
|
24
|
-
* @param {number} [options.maxResponseTimeMs=
|
|
25
|
-
* @param {number} [options.batchSize=
|
|
24
|
+
* @param {number} [options.maxResponseTimeMs=8000] - Max response time for batch
|
|
25
|
+
* @param {number} [options.batchSize=5] - Number of challenges per batch
|
|
26
26
|
* @param {number} [options.minPassCount] - Minimum challenges to pass (default: all)
|
|
27
27
|
* @param {Function} [options.onVerified] - Callback when agent is verified
|
|
28
28
|
* @param {Function} [options.onFailed] - Callback when verification fails
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aap-agent-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "Server middleware for Agent Attestation Protocol - verify AI agents",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"homepage": "https://github.com/ira-hash/agent-attestation-protocol#readme",
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"aap-agent-core": "^2.
|
|
24
|
+
"aap-agent-core": "^2.5.0"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
27
|
"express": "^4.18.0 || ^5.0.0"
|