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 CHANGED
@@ -1,11 +1,15 @@
1
1
  /**
2
- * @aap/server - Challenge Generator v2.0
2
+ * @aap/server - Challenge Generator v2.5
3
3
  *
4
- * "Deterministic Instruction Following"
5
- * - Natural language instructions (requires LLM to understand)
6
- * - Deterministic answers (server knows the correct answer)
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
- * Principle: Instructions in natural language, but answers are verifiable.
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 and respond as a JSON array.
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: `${template.text}
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 input = nonce.slice(0, 6);
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: `${instruction}
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: `${template.text}
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: `${instruction}
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: `${instruction}
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 = 3; // Number of challenges per batch
391
- export const MAX_RESPONSE_TIME_MS = 12000; // 12 seconds for batch (avg 4s per challenge)
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=12000] - Max response time for batch
25
- * @param {number} [options.batchSize=3] - Number of challenges per batch
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.0.0",
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.0.0"
24
+ "aap-agent-core": "^2.5.0"
25
25
  },
26
26
  "peerDependencies": {
27
27
  "express": "^4.18.0 || ^5.0.0"