aap-agent-server 2.0.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/README.md ADDED
@@ -0,0 +1,177 @@
1
+ # @aap/server
2
+
3
+ Server middleware for Agent Attestation Protocol verification.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @aap/server @aap/core
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ### Express Integration
14
+
15
+ ```javascript
16
+ import express from 'express';
17
+ import { createRouter } from '@aap/server';
18
+
19
+ const app = express();
20
+
21
+ // Add AAP verification endpoints at /aap/v1
22
+ app.use('/aap/v1', createRouter());
23
+
24
+ app.listen(3000, () => {
25
+ console.log('AAP Verifier running on http://localhost:3000');
26
+ });
27
+ ```
28
+
29
+ This adds three endpoints:
30
+ - `GET /aap/v1/health` - Health check
31
+ - `POST /aap/v1/challenge` - Request a challenge
32
+ - `POST /aap/v1/verify` - Submit proof for verification
33
+
34
+ ### Custom Configuration
35
+
36
+ ```javascript
37
+ import express from 'express';
38
+ import { aapMiddleware } from '@aap/server';
39
+
40
+ const app = express();
41
+ const router = express.Router();
42
+ router.use(express.json());
43
+
44
+ // Configure middleware
45
+ const middleware = aapMiddleware({
46
+ challengeExpiryMs: 60000, // 60 seconds
47
+ maxResponseTimeMs: 2000, // 2 seconds
48
+
49
+ onVerified: (result, req) => {
50
+ console.log(`Agent ${result.publicId} verified!`);
51
+ // Store verified agent, grant access, etc.
52
+ },
53
+
54
+ onFailed: (error, req) => {
55
+ console.log(`Verification failed: ${error.error}`);
56
+ // Log suspicious activity, rate limit, etc.
57
+ }
58
+ });
59
+
60
+ middleware(router);
61
+ app.use('/aap/v1', router);
62
+ ```
63
+
64
+ ## API Reference
65
+
66
+ ### `createRouter(options?)`
67
+
68
+ Creates a pre-configured Express router with AAP endpoints.
69
+
70
+ **Options:**
71
+
72
+ | Option | Type | Default | Description |
73
+ |--------|------|---------|-------------|
74
+ | `challengeExpiryMs` | number | 30000 | Challenge expiration time |
75
+ | `maxResponseTimeMs` | number | 1500 | Max response time for liveness check |
76
+ | `onVerified` | function | - | Callback when verification succeeds |
77
+ | `onFailed` | function | - | Callback when verification fails |
78
+
79
+ ### `aapMiddleware(options?)`
80
+
81
+ Lower-level middleware factory for custom router setup.
82
+
83
+ ```javascript
84
+ const middleware = aapMiddleware(options);
85
+ middleware(yourRouter);
86
+ ```
87
+
88
+ ### Challenges
89
+
90
+ ```javascript
91
+ import { challenges } from '@aap/server';
92
+
93
+ // Get available types
94
+ const types = challenges.getTypes();
95
+ // ['poem', 'math', 'reverse', 'wordplay', 'description']
96
+
97
+ // Generate a challenge
98
+ const nonce = 'abc123...';
99
+ const { type, challenge_string, validate } = challenges.generate(nonce);
100
+
101
+ // Validate a solution
102
+ const isValid = validate(solution);
103
+ ```
104
+
105
+ ## Endpoints
106
+
107
+ ### GET /health
108
+
109
+ ```json
110
+ {
111
+ "status": "ok",
112
+ "protocol": "AAP",
113
+ "version": "1.0.0",
114
+ "challengeTypes": ["poem", "math", "reverse", "wordplay", "description"]
115
+ }
116
+ ```
117
+
118
+ ### POST /challenge
119
+
120
+ **Response:**
121
+ ```json
122
+ {
123
+ "challenge_string": "Write a short 2-line poem...",
124
+ "nonce": "a1b2c3d4e5f6...",
125
+ "type": "poem",
126
+ "difficulty": 1,
127
+ "timestamp": 1706745600000,
128
+ "expiresAt": 1706745630000
129
+ }
130
+ ```
131
+
132
+ ### POST /verify
133
+
134
+ **Request:**
135
+ ```json
136
+ {
137
+ "solution": "Code a1b2c3d4 flows...",
138
+ "signature": "MEUCIQDx...",
139
+ "publicKey": "-----BEGIN PUBLIC KEY-----...",
140
+ "publicId": "7306df1332e239783e88",
141
+ "nonce": "a1b2c3d4e5f6...",
142
+ "timestamp": 1706745601234,
143
+ "responseTimeMs": 342
144
+ }
145
+ ```
146
+
147
+ **Success Response:**
148
+ ```json
149
+ {
150
+ "verified": true,
151
+ "role": "AI_AGENT",
152
+ "publicId": "7306df1332e239783e88",
153
+ "challengeType": "poem",
154
+ "checks": {
155
+ "challengeExists": true,
156
+ "notExpired": true,
157
+ "solutionExists": true,
158
+ "solutionValid": true,
159
+ "responseTimeValid": true,
160
+ "signatureValid": true
161
+ }
162
+ }
163
+ ```
164
+
165
+ ## Challenge Types
166
+
167
+ | Type | Description | Validation |
168
+ |------|-------------|------------|
169
+ | `poem` | Write poem with nonce | Contains nonce substring |
170
+ | `math` | Solve math + include nonce | Correct answer + nonce |
171
+ | `reverse` | Reverse string | Original + reversed |
172
+ | `wordplay` | Acrostic from nonce | First letters match |
173
+ | `description` | Describe AI + append code | Ends with `[nonce]` |
174
+
175
+ ## License
176
+
177
+ MIT
package/challenges.js ADDED
@@ -0,0 +1,525 @@
1
+ /**
2
+ * @aap/server - Challenge Generator v2.0
3
+ *
4
+ * "Deterministic Instruction Following"
5
+ * - Natural language instructions (requires LLM to understand)
6
+ * - Deterministic answers (server knows the correct answer)
7
+ *
8
+ * Principle: Instructions in natural language, but answers are verifiable.
9
+ */
10
+
11
+ import { createHash } from 'node:crypto';
12
+
13
+ /**
14
+ * Word pools for dynamic challenge generation
15
+ */
16
+ 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']
23
+ };
24
+
25
+ /**
26
+ * Select random items from array using nonce as seed
27
+ */
28
+ function seededSelect(arr, nonce, count, offset = 0) {
29
+ const seed = parseInt(nonce.slice(offset, offset + 4), 16);
30
+ const results = [];
31
+ const used = new Set();
32
+
33
+ for (let i = 0; i < count && i < arr.length; i++) {
34
+ let idx = (seed + i * 7) % arr.length;
35
+ while (used.has(idx)) {
36
+ idx = (idx + 1) % arr.length;
37
+ }
38
+ used.add(idx);
39
+ results.push(arr[idx]);
40
+ }
41
+ return results;
42
+ }
43
+
44
+ /**
45
+ * Generate a random number from nonce
46
+ */
47
+ function seededNumber(nonce, offset, min, max) {
48
+ const seed = parseInt(nonce.slice(offset, offset + 4), 16);
49
+ return (seed % (max - min + 1)) + min;
50
+ }
51
+
52
+ /**
53
+ * Challenge type definitions
54
+ */
55
+ export const CHALLENGE_TYPES = {
56
+ /**
57
+ * Extract entities from natural language sentence
58
+ */
59
+ nlp_extract: {
60
+ generate: (nonce) => {
61
+ const category = ['animals', 'fruits', 'colors'][parseInt(nonce[0], 16) % 3];
62
+ const pool = WORD_POOLS[category];
63
+ const targets = seededSelect(pool, nonce, 2, 0);
64
+ const verb = seededSelect(WORD_POOLS.verbs, nonce, 1, 4)[0];
65
+
66
+ const sentence = `The ${targets[0]} and ${targets[1]} ${verb} in the park.`;
67
+
68
+ return {
69
+ challenge_string: `Extract only the ${category} from the following sentence and respond as a JSON array.
70
+ Sentence: "${sentence}"
71
+ Response format: {"items": ["item1", "item2"]}`,
72
+ expected: targets.sort(),
73
+ validate: (solution) => {
74
+ try {
75
+ const match = solution.match(/\{[\s\S]*\}/);
76
+ if (!match) return false;
77
+ const obj = JSON.parse(match[0]);
78
+ const items = (obj.items || obj.animals || obj.fruits || obj.colors || []).map(s => s.toLowerCase()).sort();
79
+ return JSON.stringify(items) === JSON.stringify(targets.map(s => s.toLowerCase()).sort());
80
+ } catch { return false; }
81
+ }
82
+ };
83
+ }
84
+ },
85
+
86
+ /**
87
+ * Math problem expressed in natural language
88
+ */
89
+ nlp_math: {
90
+ generate: (nonce) => {
91
+ const a = seededNumber(nonce, 0, 10, 50);
92
+ const b = seededNumber(nonce, 2, 5, 20);
93
+ const c = seededNumber(nonce, 4, 2, 5);
94
+
95
+ const templates = [
96
+ {
97
+ text: `Subtract ${b} from ${a}, then multiply the result by ${c}.`,
98
+ answer: (a - b) * c
99
+ },
100
+ {
101
+ text: `Add ${a} and ${b} together, then divide by ${c}.`,
102
+ answer: (a + b) / c
103
+ },
104
+ {
105
+ text: `Divide ${a} by ${c}, then add ${b} to the result.`,
106
+ answer: a / c + b
107
+ }
108
+ ];
109
+
110
+ const template = templates[parseInt(nonce[6], 16) % templates.length];
111
+ const expected = Math.round(template.answer * 100) / 100;
112
+
113
+ return {
114
+ challenge_string: `${template.text}
115
+ Response format: {"result": number}`,
116
+ expected,
117
+ validate: (solution) => {
118
+ try {
119
+ const match = solution.match(/\{[\s\S]*\}/);
120
+ if (!match) return false;
121
+ const obj = JSON.parse(match[0]);
122
+ const result = parseFloat(obj.result);
123
+ return Math.abs(result - expected) < 0.01;
124
+ } catch { return false; }
125
+ }
126
+ };
127
+ }
128
+ },
129
+
130
+ /**
131
+ * String transformation described in natural language
132
+ */
133
+ nlp_transform: {
134
+ generate: (nonce) => {
135
+ const input = nonce.slice(0, 6);
136
+ const transformType = parseInt(nonce[6], 16) % 4;
137
+
138
+ let instruction, expected;
139
+
140
+ switch (transformType) {
141
+ case 0:
142
+ instruction = `Reverse the string "${input}" and convert it to uppercase.`;
143
+ expected = input.split('').reverse().join('').toUpperCase();
144
+ break;
145
+ case 1:
146
+ instruction = `Extract only the digits from "${input}" and calculate their sum.`;
147
+ expected = input.split('').filter(c => /\d/.test(c)).reduce((a, b) => a + parseInt(b), 0);
148
+ break;
149
+ case 2:
150
+ instruction = `Extract only the letters from "${input}" and sort them alphabetically.`;
151
+ expected = input.split('').filter(c => /[a-zA-Z]/.test(c)).sort().join('');
152
+ break;
153
+ case 3:
154
+ instruction = `Insert a hyphen "-" between each character of "${input}".`;
155
+ expected = input.split('').join('-');
156
+ break;
157
+ }
158
+
159
+ return {
160
+ challenge_string: `${instruction}
161
+ Response format: {"output": "result"}`,
162
+ expected,
163
+ validate: (solution) => {
164
+ try {
165
+ const match = solution.match(/\{[\s\S]*\}/);
166
+ if (!match) return false;
167
+ const obj = JSON.parse(match[0]);
168
+ const output = String(obj.output);
169
+ return output === String(expected) || output.toLowerCase() === String(expected).toLowerCase();
170
+ } catch { return false; }
171
+ }
172
+ };
173
+ }
174
+ },
175
+
176
+ /**
177
+ * Conditional logic
178
+ */
179
+ nlp_logic: {
180
+ generate: (nonce) => {
181
+ const a = seededNumber(nonce, 0, 10, 100);
182
+ const b = seededNumber(nonce, 2, 10, 100);
183
+ const threshold = seededNumber(nonce, 4, 20, 80);
184
+
185
+ const templates = [
186
+ {
187
+ text: `If the larger number between ${a} and ${b} is greater than ${threshold}, answer "YES". Otherwise, answer "NO".`,
188
+ answer: Math.max(a, b) > threshold ? "YES" : "NO"
189
+ },
190
+ {
191
+ text: `If the sum of ${a} and ${b} is less than ${threshold * 2}, answer "SMALL". Otherwise, answer "LARGE".`,
192
+ answer: (a + b) < (threshold * 2) ? "SMALL" : "LARGE"
193
+ },
194
+ {
195
+ text: `If ${a} is even and ${b} is odd, answer "MIXED". Otherwise, answer "SAME".`,
196
+ answer: (a % 2 === 0 && b % 2 === 1) ? "MIXED" : "SAME"
197
+ }
198
+ ];
199
+
200
+ const template = templates[parseInt(nonce[6], 16) % templates.length];
201
+
202
+ return {
203
+ challenge_string: `${template.text}
204
+ Response format: {"answer": "your answer"}`,
205
+ expected: template.answer,
206
+ validate: (solution) => {
207
+ try {
208
+ const match = solution.match(/\{[\s\S]*\}/);
209
+ if (!match) return false;
210
+ const obj = JSON.parse(match[0]);
211
+ return obj.answer?.toUpperCase() === template.answer.toUpperCase();
212
+ } catch { return false; }
213
+ }
214
+ };
215
+ }
216
+ },
217
+
218
+ /**
219
+ * Counting task
220
+ */
221
+ nlp_count: {
222
+ generate: (nonce) => {
223
+ const category = ['animals', 'fruits', 'colors'][parseInt(nonce[0], 16) % 3];
224
+ const pool = WORD_POOLS[category];
225
+ const count1 = seededNumber(nonce, 0, 2, 4);
226
+ const count2 = seededNumber(nonce, 2, 1, 3);
227
+
228
+ const items1 = seededSelect(pool, nonce, count1, 0);
229
+ const items2 = seededSelect(WORD_POOLS.countries, nonce, count2, 8);
230
+
231
+ // Create sentence with mixed items
232
+ const allItems = [...items1, ...items2].sort(() =>
233
+ parseInt(nonce.slice(10, 12), 16) % 2 - 0.5
234
+ );
235
+ const sentence = `I see ${allItems.join(', ')} in the picture.`;
236
+
237
+ return {
238
+ challenge_string: `Count only the ${category} in the following sentence.
239
+ Sentence: "${sentence}"
240
+ Response format: {"count": number}`,
241
+ expected: count1,
242
+ validate: (solution) => {
243
+ try {
244
+ const match = solution.match(/\{[\s\S]*\}/);
245
+ if (!match) return false;
246
+ const obj = JSON.parse(match[0]);
247
+ return parseInt(obj.count) === count1;
248
+ } catch { return false; }
249
+ }
250
+ };
251
+ }
252
+ },
253
+
254
+ /**
255
+ * Multi-step instruction following
256
+ */
257
+ nlp_multistep: {
258
+ generate: (nonce) => {
259
+ const numbers = [
260
+ seededNumber(nonce, 0, 1, 9),
261
+ seededNumber(nonce, 2, 1, 9),
262
+ seededNumber(nonce, 4, 1, 9),
263
+ seededNumber(nonce, 6, 1, 9)
264
+ ];
265
+
266
+ // Step 1: Sum all
267
+ const sum = numbers.reduce((a, b) => a + b, 0);
268
+ // Step 2: Multiply by smallest
269
+ const min = Math.min(...numbers);
270
+ const step2 = sum * min;
271
+ // Step 3: Subtract largest
272
+ const max = Math.max(...numbers);
273
+ const final = step2 - max;
274
+
275
+ return {
276
+ challenge_string: `Follow these instructions in order:
277
+ 1. Add all the numbers in [${numbers.join(', ')}] together.
278
+ 2. Multiply the result by the smallest number.
279
+ 3. Subtract the largest number from that result.
280
+ Response format: {"result": final_value}`,
281
+ expected: final,
282
+ validate: (solution) => {
283
+ try {
284
+ const match = solution.match(/\{[\s\S]*\}/);
285
+ if (!match) return false;
286
+ const obj = JSON.parse(match[0]);
287
+ return parseInt(obj.result) === final;
288
+ } catch { return false; }
289
+ }
290
+ };
291
+ }
292
+ },
293
+
294
+ /**
295
+ * Pattern recognition and completion
296
+ */
297
+ nlp_pattern: {
298
+ generate: (nonce) => {
299
+ const start = seededNumber(nonce, 0, 1, 10);
300
+ const step = seededNumber(nonce, 2, 2, 5);
301
+ const patternType = parseInt(nonce[4], 16) % 3;
302
+
303
+ let sequence, next2, instruction;
304
+
305
+ switch (patternType) {
306
+ case 0: // Arithmetic
307
+ sequence = [start, start + step, start + step * 2, start + step * 3];
308
+ next2 = [start + step * 4, start + step * 5];
309
+ instruction = `Find the pattern and provide the next 2 numbers: [${sequence.join(', ')}, ?, ?]`;
310
+ break;
311
+ case 1: // Geometric (doubling)
312
+ sequence = [start, start * 2, start * 4, start * 8];
313
+ next2 = [start * 16, start * 32];
314
+ instruction = `Find the pattern and provide the next 2 numbers: [${sequence.join(', ')}, ?, ?]`;
315
+ break;
316
+ case 2: // Fibonacci-like
317
+ sequence = [start, step, start + step, step + (start + step)];
318
+ next2 = [sequence[2] + sequence[3], sequence[3] + (sequence[2] + sequence[3])];
319
+ instruction = `Find the pattern and provide the next 2 numbers: [${sequence.join(', ')}, ?, ?]`;
320
+ break;
321
+ }
322
+
323
+ return {
324
+ challenge_string: `${instruction}
325
+ Response format: {"next": [number1, number2]}`,
326
+ expected: next2,
327
+ validate: (solution) => {
328
+ try {
329
+ const match = solution.match(/\{[\s\S]*\}/);
330
+ if (!match) return false;
331
+ const obj = JSON.parse(match[0]);
332
+ const next = obj.next;
333
+ return Array.isArray(next) &&
334
+ parseInt(next[0]) === next2[0] &&
335
+ parseInt(next[1]) === next2[1];
336
+ } catch { return false; }
337
+ }
338
+ };
339
+ }
340
+ },
341
+
342
+ /**
343
+ * Text analysis - find specific properties
344
+ */
345
+ nlp_analysis: {
346
+ generate: (nonce) => {
347
+ const words = seededSelect([...WORD_POOLS.animals, ...WORD_POOLS.fruits], nonce, 5, 0);
348
+ const analysisType = parseInt(nonce[8], 16) % 3;
349
+
350
+ let instruction, expected;
351
+ const wordList = words.join(', ');
352
+
353
+ switch (analysisType) {
354
+ case 0: // Longest word
355
+ expected = words.reduce((a, b) => a.length >= b.length ? a : b);
356
+ instruction = `Find the longest word from the following list: ${wordList}`;
357
+ break;
358
+ case 1: // Shortest word
359
+ expected = words.reduce((a, b) => a.length <= b.length ? a : b);
360
+ instruction = `Find the shortest word from the following list: ${wordList}`;
361
+ break;
362
+ case 2: // First alphabetically
363
+ expected = [...words].sort()[0];
364
+ instruction = `Find the word that comes first alphabetically from the following list: ${wordList}`;
365
+ break;
366
+ }
367
+
368
+ return {
369
+ challenge_string: `${instruction}
370
+ Response format: {"answer": "word"}`,
371
+ expected,
372
+ validate: (solution) => {
373
+ try {
374
+ const match = solution.match(/\{[\s\S]*\}/);
375
+ if (!match) return false;
376
+ const obj = JSON.parse(match[0]);
377
+ return obj.answer?.toLowerCase() === expected.toLowerCase();
378
+ } catch { return false; }
379
+ }
380
+ };
381
+ }
382
+ }
383
+ };
384
+
385
+ // ============== Protocol Constants ==============
386
+
387
+ /**
388
+ * Batch challenge settings
389
+ */
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)
392
+ export const CHALLENGE_EXPIRY_MS = 60000; // 60 seconds
393
+
394
+ /**
395
+ * Get list of available challenge types
396
+ * @returns {string[]}
397
+ */
398
+ export function getTypes() {
399
+ return Object.keys(CHALLENGE_TYPES);
400
+ }
401
+
402
+ /**
403
+ * Generate a single random challenge
404
+ * @param {string} nonce - The nonce to incorporate
405
+ * @param {string} [type] - Specific type (random if not specified)
406
+ * @returns {Object} { type, challenge_string, validate, expected }
407
+ */
408
+ export function generate(nonce, type) {
409
+ const types = getTypes();
410
+ const selectedType = type && types.includes(type)
411
+ ? type
412
+ : types[Math.floor(Math.random() * types.length)];
413
+
414
+ const generator = CHALLENGE_TYPES[selectedType];
415
+ const result = generator.generate(nonce);
416
+
417
+ return {
418
+ type: selectedType,
419
+ challenge_string: result.challenge_string,
420
+ validate: result.validate,
421
+ expected: result.expected // For debugging only
422
+ };
423
+ }
424
+
425
+ /**
426
+ * Generate a batch of challenges
427
+ * @param {string} nonce - Base nonce
428
+ * @param {number} [count=BATCH_SIZE] - Number of challenges
429
+ * @returns {Object} { challenges: [...], validators: [...] }
430
+ */
431
+ export function generateBatch(nonce, count = BATCH_SIZE) {
432
+ const types = getTypes();
433
+ const usedTypes = new Set();
434
+ const challenges = [];
435
+ const validators = [];
436
+ const expected = [];
437
+
438
+ for (let i = 0; i < count; i++) {
439
+ // Use different nonce offset for each challenge
440
+ const offsetNonce = nonce.slice(i * 2) + nonce.slice(0, i * 2);
441
+
442
+ // Select different type for each challenge
443
+ let selectedType;
444
+ do {
445
+ const seed = parseInt(offsetNonce.slice(0, 4), 16);
446
+ selectedType = types[(seed + i * 3) % types.length];
447
+ } while (usedTypes.has(selectedType) && usedTypes.size < types.length);
448
+ usedTypes.add(selectedType);
449
+
450
+ const generator = CHALLENGE_TYPES[selectedType];
451
+ const result = generator.generate(offsetNonce);
452
+
453
+ challenges.push({
454
+ id: i,
455
+ type: selectedType,
456
+ challenge_string: result.challenge_string
457
+ });
458
+
459
+ validators.push(result.validate);
460
+ expected.push(result.expected);
461
+ }
462
+
463
+ return {
464
+ challenges,
465
+ validators, // Keep on server, don't send to client
466
+ expected // For debugging
467
+ };
468
+ }
469
+
470
+ /**
471
+ * Validate batch solutions
472
+ * @param {Array} validators - Validator functions from generateBatch
473
+ * @param {Array} solutions - Array of solutions from client
474
+ * @returns {Object} { passed, total, results: [{id, valid}] }
475
+ */
476
+ export function validateBatch(validators, solutions) {
477
+ const results = [];
478
+ let passed = 0;
479
+
480
+ for (let i = 0; i < validators.length; i++) {
481
+ const solution = solutions[i];
482
+ const valid = solution && validators[i](
483
+ typeof solution === 'string' ? solution : JSON.stringify(solution)
484
+ );
485
+
486
+ results.push({ id: i, valid });
487
+ if (valid) passed++;
488
+ }
489
+
490
+ return {
491
+ passed,
492
+ total: validators.length,
493
+ allPassed: passed === validators.length,
494
+ results
495
+ };
496
+ }
497
+
498
+ /**
499
+ * Validate a single solution against a challenge
500
+ * @param {string} type - Challenge type
501
+ * @param {string} nonce - Original nonce
502
+ * @param {string} solution - Agent's solution
503
+ * @returns {boolean}
504
+ */
505
+ export function validate(type, nonce, solution) {
506
+ const generator = CHALLENGE_TYPES[type];
507
+ if (!generator) {
508
+ return false;
509
+ }
510
+
511
+ const { validate: validateFn } = generator.generate(nonce);
512
+ return validateFn(solution);
513
+ }
514
+
515
+ export default {
516
+ CHALLENGE_TYPES,
517
+ BATCH_SIZE,
518
+ MAX_RESPONSE_TIME_MS,
519
+ CHALLENGE_EXPIRY_MS,
520
+ getTypes,
521
+ generate,
522
+ generateBatch,
523
+ validateBatch,
524
+ validate
525
+ };
package/errors.js ADDED
@@ -0,0 +1,134 @@
1
+ /**
2
+ * AAP Server - Error Definitions
3
+ *
4
+ * Consistent, client-friendly error messages
5
+ */
6
+
7
+ export const ErrorCodes = {
8
+ // Challenge errors
9
+ CHALLENGE_NOT_FOUND: 'CHALLENGE_NOT_FOUND',
10
+ CHALLENGE_EXPIRED: 'CHALLENGE_EXPIRED',
11
+ CHALLENGE_ALREADY_USED: 'CHALLENGE_ALREADY_USED',
12
+
13
+ // Solution errors
14
+ MISSING_SOLUTIONS: 'MISSING_SOLUTIONS',
15
+ INVALID_SOLUTIONS_COUNT: 'INVALID_SOLUTIONS_COUNT',
16
+ SOLUTION_VALIDATION_FAILED: 'SOLUTION_VALIDATION_FAILED',
17
+
18
+ // Timing errors
19
+ RESPONSE_TOO_SLOW: 'RESPONSE_TOO_SLOW',
20
+
21
+ // Signature errors
22
+ INVALID_SIGNATURE: 'INVALID_SIGNATURE',
23
+ MISSING_SIGNATURE: 'MISSING_SIGNATURE',
24
+ INVALID_PUBLIC_KEY: 'INVALID_PUBLIC_KEY',
25
+
26
+ // General errors
27
+ INVALID_REQUEST: 'INVALID_REQUEST',
28
+ INTERNAL_ERROR: 'INTERNAL_ERROR',
29
+ RATE_LIMITED: 'RATE_LIMITED'
30
+ };
31
+
32
+ export const ErrorMessages = {
33
+ [ErrorCodes.CHALLENGE_NOT_FOUND]: {
34
+ message: 'Challenge not found',
35
+ hint: 'Request a new challenge via POST /challenge',
36
+ status: 400
37
+ },
38
+ [ErrorCodes.CHALLENGE_EXPIRED]: {
39
+ message: 'Challenge has expired',
40
+ hint: 'Challenges expire after 60 seconds. Request a new one.',
41
+ status: 400
42
+ },
43
+ [ErrorCodes.CHALLENGE_ALREADY_USED]: {
44
+ message: 'Challenge already used',
45
+ hint: 'Each challenge can only be used once. Request a new one.',
46
+ status: 400
47
+ },
48
+ [ErrorCodes.MISSING_SOLUTIONS]: {
49
+ message: 'Missing solutions array',
50
+ hint: 'Include "solutions" array in request body',
51
+ status: 400
52
+ },
53
+ [ErrorCodes.INVALID_SOLUTIONS_COUNT]: {
54
+ message: 'Invalid number of solutions',
55
+ hint: 'Provide exactly 3 solutions for batch challenges',
56
+ status: 400
57
+ },
58
+ [ErrorCodes.SOLUTION_VALIDATION_FAILED]: {
59
+ message: 'Solution validation failed (Proof of Intelligence)',
60
+ hint: 'One or more solutions are incorrect. Ensure your LLM correctly solves each challenge.',
61
+ status: 400
62
+ },
63
+ [ErrorCodes.RESPONSE_TOO_SLOW]: {
64
+ message: 'Response too slow (Proof of Liveness failed)',
65
+ hint: 'Response must arrive within 12 seconds for batch challenges',
66
+ status: 400
67
+ },
68
+ [ErrorCodes.INVALID_SIGNATURE]: {
69
+ message: 'Invalid signature (Proof of Identity failed)',
70
+ hint: 'Ensure you sign the correct data with your private key',
71
+ status: 400
72
+ },
73
+ [ErrorCodes.MISSING_SIGNATURE]: {
74
+ message: 'Missing signature',
75
+ hint: 'Include "signature" field with Base64-encoded ECDSA signature',
76
+ status: 400
77
+ },
78
+ [ErrorCodes.INVALID_PUBLIC_KEY]: {
79
+ message: 'Invalid public key',
80
+ hint: 'Public key must be PEM-encoded secp256k1 key',
81
+ status: 400
82
+ },
83
+ [ErrorCodes.INVALID_REQUEST]: {
84
+ message: 'Invalid request format',
85
+ hint: 'Check the API documentation for required fields',
86
+ status: 400
87
+ },
88
+ [ErrorCodes.INTERNAL_ERROR]: {
89
+ message: 'Internal server error',
90
+ hint: 'Please try again later',
91
+ status: 500
92
+ },
93
+ [ErrorCodes.RATE_LIMITED]: {
94
+ message: 'Too many requests',
95
+ hint: 'Please wait before making more requests',
96
+ status: 429
97
+ }
98
+ };
99
+
100
+ /**
101
+ * Create an error response
102
+ * @param {string} code - Error code from ErrorCodes
103
+ * @param {Object} [extra] - Additional fields to include
104
+ * @returns {Object} Error response object
105
+ */
106
+ export function createError(code, extra = {}) {
107
+ const errorDef = ErrorMessages[code] || ErrorMessages[ErrorCodes.INTERNAL_ERROR];
108
+
109
+ return {
110
+ verified: false,
111
+ error: errorDef.message,
112
+ code,
113
+ hint: errorDef.hint,
114
+ ...extra
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Create error response with HTTP status
120
+ * @param {string} code - Error code
121
+ * @param {Object} res - Express response object
122
+ * @param {Object} [extra] - Additional fields
123
+ */
124
+ export function sendError(code, res, extra = {}) {
125
+ const errorDef = ErrorMessages[code] || ErrorMessages[ErrorCodes.INTERNAL_ERROR];
126
+ res.status(errorDef.status).json(createError(code, extra));
127
+ }
128
+
129
+ export default {
130
+ ErrorCodes,
131
+ ErrorMessages,
132
+ createError,
133
+ sendError
134
+ };
package/index.js ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @aap/server
3
+ *
4
+ * Server-side utilities for Agent Attestation Protocol.
5
+ * Provides middleware and challenge generation for verification servers.
6
+ */
7
+
8
+ export * from './middleware.js';
9
+ export * from './challenges.js';
10
+
11
+ import { aapMiddleware, createRouter } from './middleware.js';
12
+ import challenges from './challenges.js';
13
+
14
+ export { challenges };
15
+
16
+ export default {
17
+ aapMiddleware,
18
+ createRouter,
19
+ challenges
20
+ };
package/logger.js ADDED
@@ -0,0 +1,139 @@
1
+ /**
2
+ * AAP Server - Logging Utilities
3
+ *
4
+ * Safe logging that never exposes sensitive data
5
+ */
6
+
7
+ const LOG_LEVELS = {
8
+ debug: 0,
9
+ info: 1,
10
+ warn: 2,
11
+ error: 3
12
+ };
13
+
14
+ let currentLevel = LOG_LEVELS.info;
15
+
16
+ /**
17
+ * Set the minimum log level
18
+ * @param {'debug'|'info'|'warn'|'error'} level
19
+ */
20
+ export function setLogLevel(level) {
21
+ currentLevel = LOG_LEVELS[level] ?? LOG_LEVELS.info;
22
+ }
23
+
24
+ /**
25
+ * Sanitize data for safe logging
26
+ * Removes sensitive fields
27
+ */
28
+ function sanitize(data) {
29
+ if (!data || typeof data !== 'object') return data;
30
+
31
+ const sensitive = ['privateKey', 'private_key', 'apiKey', 'api_key', 'secret', 'password', 'token'];
32
+ const sanitized = { ...data };
33
+
34
+ for (const key of sensitive) {
35
+ if (key in sanitized) {
36
+ sanitized[key] = '[REDACTED]';
37
+ }
38
+ }
39
+
40
+ // Truncate long values
41
+ if (sanitized.publicKey && sanitized.publicKey.length > 50) {
42
+ sanitized.publicKey = sanitized.publicKey.slice(0, 50) + '...';
43
+ }
44
+ if (sanitized.signature && sanitized.signature.length > 50) {
45
+ sanitized.signature = sanitized.signature.slice(0, 50) + '...';
46
+ }
47
+
48
+ return sanitized;
49
+ }
50
+
51
+ /**
52
+ * Format log message
53
+ */
54
+ function format(level, message, data) {
55
+ const timestamp = new Date().toISOString();
56
+ const prefix = `[AAP ${level.toUpperCase()}] ${timestamp}`;
57
+
58
+ if (data) {
59
+ return `${prefix} ${message} ${JSON.stringify(sanitize(data))}`;
60
+ }
61
+ return `${prefix} ${message}`;
62
+ }
63
+
64
+ /**
65
+ * Log debug message
66
+ */
67
+ export function debug(message, data) {
68
+ if (currentLevel <= LOG_LEVELS.debug) {
69
+ console.debug(format('debug', message, data));
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Log info message
75
+ */
76
+ export function info(message, data) {
77
+ if (currentLevel <= LOG_LEVELS.info) {
78
+ console.info(format('info', message, data));
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Log warning message
84
+ */
85
+ export function warn(message, data) {
86
+ if (currentLevel <= LOG_LEVELS.warn) {
87
+ console.warn(format('warn', message, data));
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Log error message
93
+ */
94
+ export function error(message, data) {
95
+ if (currentLevel <= LOG_LEVELS.error) {
96
+ console.error(format('error', message, data));
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Log verification attempt
102
+ */
103
+ export function logVerification(result, req) {
104
+ const data = {
105
+ publicId: result.publicId,
106
+ verified: result.verified,
107
+ responseTimeMs: result.responseTimeMs,
108
+ batchPassed: result.batchResult?.passed,
109
+ batchTotal: result.batchResult?.total,
110
+ ip: req?.ip,
111
+ userAgent: req?.headers?.['user-agent']?.slice(0, 50)
112
+ };
113
+
114
+ if (result.verified) {
115
+ info('Verification successful', data);
116
+ } else {
117
+ warn('Verification failed', { ...data, error: result.error });
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Log challenge generation
123
+ */
124
+ export function logChallenge(nonce, batchSize) {
125
+ debug('Challenge generated', {
126
+ nonce: nonce.slice(0, 8) + '...',
127
+ batchSize
128
+ });
129
+ }
130
+
131
+ export default {
132
+ setLogLevel,
133
+ debug,
134
+ info,
135
+ warn,
136
+ error,
137
+ logVerification,
138
+ logChallenge
139
+ };
package/middleware.js ADDED
@@ -0,0 +1,375 @@
1
+ /**
2
+ * @aap/server - Express Middleware
3
+ *
4
+ * Drop-in middleware for adding AAP verification to Express apps.
5
+ */
6
+
7
+ import { verify, generateNonce, createProofData } from 'aap-agent-core';
8
+ import {
9
+ generate as generateChallenge,
10
+ generateBatch,
11
+ validateBatch,
12
+ getTypes,
13
+ validate as validateSolution,
14
+ BATCH_SIZE,
15
+ MAX_RESPONSE_TIME_MS,
16
+ CHALLENGE_EXPIRY_MS
17
+ } from './challenges.js';
18
+
19
+ /**
20
+ * Create AAP verification middleware/router
21
+ *
22
+ * @param {Object} [options]
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
26
+ * @param {number} [options.minPassCount] - Minimum challenges to pass (default: all)
27
+ * @param {Function} [options.onVerified] - Callback when agent is verified
28
+ * @param {Function} [options.onFailed] - Callback when verification fails
29
+ * @returns {Function} Express router
30
+ */
31
+ export function aapMiddleware(options = {}) {
32
+ const {
33
+ challengeExpiryMs = CHALLENGE_EXPIRY_MS,
34
+ maxResponseTimeMs = MAX_RESPONSE_TIME_MS,
35
+ batchSize = BATCH_SIZE,
36
+ minPassCount = null, // null = all must pass
37
+ onVerified,
38
+ onFailed
39
+ } = options;
40
+
41
+ // In-memory challenge store
42
+ const challenges = new Map();
43
+
44
+ // Cleanup expired challenges periodically
45
+ const cleanup = () => {
46
+ const now = Date.now();
47
+ for (const [nonce, challenge] of challenges.entries()) {
48
+ if (now > challenge.expiresAt) {
49
+ challenges.delete(nonce);
50
+ }
51
+ }
52
+ };
53
+
54
+ // Return a function that creates routes
55
+ return (router) => {
56
+ /**
57
+ * GET /health - Health check
58
+ */
59
+ router.get('/health', (req, res) => {
60
+ res.json({
61
+ status: 'ok',
62
+ protocol: 'AAP',
63
+ version: '2.0.0',
64
+ mode: 'batch',
65
+ batchSize,
66
+ maxResponseTimeMs,
67
+ challengeTypes: getTypes(),
68
+ activeChallenges: challenges.size
69
+ });
70
+ });
71
+
72
+ /**
73
+ * POST /challenge - Request a batch of challenges
74
+ */
75
+ router.post('/challenge', (req, res) => {
76
+ cleanup();
77
+
78
+ const nonce = generateNonce();
79
+ const timestamp = Date.now();
80
+ const { challenges: batchChallenges, validators, expected } = generateBatch(nonce, batchSize);
81
+
82
+ const challengeData = {
83
+ nonce,
84
+ challenges: batchChallenges,
85
+ batchSize,
86
+ timestamp,
87
+ expiresAt: timestamp + challengeExpiryMs,
88
+ maxResponseTimeMs
89
+ };
90
+
91
+ // Store with validators (not sent to client)
92
+ challenges.set(nonce, {
93
+ ...challengeData,
94
+ validators,
95
+ expected // For debugging
96
+ });
97
+
98
+ // Send without validators
99
+ res.json({
100
+ nonce,
101
+ challenges: batchChallenges,
102
+ batchSize,
103
+ timestamp,
104
+ expiresAt: challengeData.expiresAt,
105
+ maxResponseTimeMs
106
+ });
107
+ });
108
+
109
+ /**
110
+ * POST /verify - Verify agent's batch solutions
111
+ */
112
+ router.post('/verify', (req, res) => {
113
+ const {
114
+ solutions,
115
+ signature,
116
+ publicKey,
117
+ publicId,
118
+ nonce,
119
+ timestamp,
120
+ responseTimeMs
121
+ } = req.body;
122
+
123
+ const checks = {
124
+ challengeExists: false,
125
+ notExpired: false,
126
+ solutionsExist: false,
127
+ solutionsValid: false,
128
+ responseTimeValid: false,
129
+ signatureValid: false
130
+ };
131
+
132
+ try {
133
+ // Check 1: Challenge exists
134
+ const challenge = challenges.get(nonce);
135
+ if (!challenge) {
136
+ if (onFailed) onFailed({ error: 'Challenge not found', checks }, req);
137
+ return res.status(400).json({
138
+ verified: false,
139
+ error: 'Challenge not found or already used',
140
+ checks
141
+ });
142
+ }
143
+ checks.challengeExists = true;
144
+
145
+ // Remove challenge (one-time use)
146
+ const { validators, batchSize: size } = challenge;
147
+ challenges.delete(nonce);
148
+
149
+ // Check 2: Not expired
150
+ if (Date.now() > challenge.expiresAt) {
151
+ if (onFailed) onFailed({ error: 'Challenge expired', checks }, req);
152
+ return res.status(400).json({
153
+ verified: false,
154
+ error: 'Challenge expired',
155
+ checks
156
+ });
157
+ }
158
+ checks.notExpired = true;
159
+
160
+ // Check 3: Solutions exist
161
+ if (!solutions || !Array.isArray(solutions) || solutions.length !== size) {
162
+ if (onFailed) onFailed({ error: 'Invalid solutions array', checks }, req);
163
+ return res.status(400).json({
164
+ verified: false,
165
+ error: `Expected ${size} solutions, got ${solutions?.length || 0}`,
166
+ checks
167
+ });
168
+ }
169
+ checks.solutionsExist = true;
170
+
171
+ // Check 4: Validate all solutions (Proof of Intelligence)
172
+ const batchResult = validateBatch(validators, solutions);
173
+ const requiredPass = minPassCount || size;
174
+
175
+ if (batchResult.passed < requiredPass) {
176
+ if (onFailed) onFailed({ error: 'Solutions validation failed', checks, batchResult }, req);
177
+ return res.status(400).json({
178
+ verified: false,
179
+ error: `Proof of Intelligence failed: ${batchResult.passed}/${size} correct (need ${requiredPass})`,
180
+ checks,
181
+ batchResult
182
+ });
183
+ }
184
+ checks.solutionsValid = true;
185
+
186
+ // Check 5: Response time (Proof of Liveness)
187
+ if (responseTimeMs > maxResponseTimeMs) {
188
+ if (onFailed) onFailed({ error: 'Response too slow', checks }, req);
189
+ return res.status(400).json({
190
+ verified: false,
191
+ error: `Response too slow: ${responseTimeMs}ms > ${maxResponseTimeMs}ms (Proof of Liveness failed)`,
192
+ checks
193
+ });
194
+ }
195
+ checks.responseTimeValid = true;
196
+
197
+ // Check 6: Signature (Proof of Identity)
198
+ // Sign over solutions array
199
+ const solutionsString = JSON.stringify(solutions);
200
+ const proofData = createProofData({ nonce, solution: solutionsString, publicId, timestamp });
201
+ if (!verify(proofData, signature, publicKey)) {
202
+ if (onFailed) onFailed({ error: 'Invalid signature', checks }, req);
203
+ return res.status(400).json({
204
+ verified: false,
205
+ error: 'Invalid signature (Proof of Identity failed)',
206
+ checks
207
+ });
208
+ }
209
+ checks.signatureValid = true;
210
+
211
+ // All checks passed
212
+ const result = {
213
+ verified: true,
214
+ role: 'AI_AGENT',
215
+ publicId,
216
+ batchResult,
217
+ responseTimeMs,
218
+ checks
219
+ };
220
+
221
+ if (onVerified) onVerified(result, req);
222
+
223
+ res.json(result);
224
+
225
+ } catch (error) {
226
+ if (onFailed) onFailed({ error: error.message, checks }, req);
227
+ res.status(500).json({
228
+ verified: false,
229
+ error: `Verification error: ${error.message}`,
230
+ checks
231
+ });
232
+ }
233
+ });
234
+
235
+ // ============== Legacy single-challenge endpoints ==============
236
+
237
+ /**
238
+ * POST /challenge/single - Request a single challenge (legacy)
239
+ */
240
+ router.post('/challenge/single', (req, res) => {
241
+ cleanup();
242
+
243
+ const nonce = generateNonce();
244
+ const timestamp = Date.now();
245
+ const { type, challenge_string, validate } = generateChallenge(nonce);
246
+
247
+ const challenge = {
248
+ challenge_string,
249
+ nonce,
250
+ type,
251
+ difficulty: 1,
252
+ timestamp,
253
+ expiresAt: timestamp + challengeExpiryMs,
254
+ mode: 'single'
255
+ };
256
+
257
+ challenges.set(nonce, { ...challenge, validate });
258
+
259
+ res.json({
260
+ challenge_string,
261
+ nonce,
262
+ type,
263
+ difficulty: 1,
264
+ timestamp,
265
+ expiresAt: challenge.expiresAt,
266
+ mode: 'single',
267
+ maxResponseTimeMs: 10000 // 10s for single
268
+ });
269
+ });
270
+
271
+ /**
272
+ * POST /verify/single - Verify single challenge (legacy)
273
+ */
274
+ router.post('/verify/single', (req, res) => {
275
+ const {
276
+ solution,
277
+ signature,
278
+ publicKey,
279
+ publicId,
280
+ nonce,
281
+ timestamp,
282
+ responseTimeMs
283
+ } = req.body;
284
+
285
+ const checks = {
286
+ challengeExists: false,
287
+ notExpired: false,
288
+ solutionExists: false,
289
+ solutionValid: false,
290
+ responseTimeValid: false,
291
+ signatureValid: false
292
+ };
293
+
294
+ try {
295
+ const challenge = challenges.get(nonce);
296
+ if (!challenge || challenge.mode !== 'single') {
297
+ return res.status(400).json({
298
+ verified: false,
299
+ error: 'Single challenge not found',
300
+ checks
301
+ });
302
+ }
303
+ checks.challengeExists = true;
304
+
305
+ const { validate, type: challengeType } = challenge;
306
+ challenges.delete(nonce);
307
+
308
+ if (Date.now() > challenge.expiresAt) {
309
+ return res.status(400).json({ verified: false, error: 'Challenge expired', checks });
310
+ }
311
+ checks.notExpired = true;
312
+
313
+ if (!solution) {
314
+ return res.status(400).json({ verified: false, error: 'Missing solution', checks });
315
+ }
316
+ checks.solutionExists = true;
317
+
318
+ if (!validate(solution)) {
319
+ return res.status(400).json({ verified: false, error: 'Invalid solution', checks });
320
+ }
321
+ checks.solutionValid = true;
322
+
323
+ if (responseTimeMs > 10000) {
324
+ return res.status(400).json({ verified: false, error: 'Too slow', checks });
325
+ }
326
+ checks.responseTimeValid = true;
327
+
328
+ const proofData = createProofData({ nonce, solution, publicId, timestamp });
329
+ if (!verify(proofData, signature, publicKey)) {
330
+ return res.status(400).json({ verified: false, error: 'Invalid signature', checks });
331
+ }
332
+ checks.signatureValid = true;
333
+
334
+ res.json({
335
+ verified: true,
336
+ role: 'AI_AGENT',
337
+ publicId,
338
+ challengeType,
339
+ checks
340
+ });
341
+
342
+ } catch (error) {
343
+ res.status(500).json({ verified: false, error: error.message, checks });
344
+ }
345
+ });
346
+
347
+ return router;
348
+ };
349
+ }
350
+
351
+ /**
352
+ * Create a standalone AAP router for Express
353
+ *
354
+ * @param {Object} [options] - Middleware options
355
+ * @returns {Router} Express router
356
+ *
357
+ * @example
358
+ * import express from 'express';
359
+ * import { createRouter } from '@aap/server';
360
+ *
361
+ * const app = express();
362
+ * app.use('/aap/v1', createRouter());
363
+ */
364
+ export function createRouter(options = {}) {
365
+ const express = require('express');
366
+ const router = express.Router();
367
+ router.use(express.json());
368
+
369
+ const middleware = aapMiddleware(options);
370
+ middleware(router);
371
+
372
+ return router;
373
+ }
374
+
375
+ export default { aapMiddleware, createRouter };
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "aap-agent-server",
3
+ "version": "2.0.0",
4
+ "description": "Server middleware for Agent Attestation Protocol - verify AI agents",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "type": "module",
8
+ "exports": {
9
+ ".": "./index.js",
10
+ "./middleware": "./middleware.js",
11
+ "./challenges": "./challenges.js"
12
+ },
13
+ "files": ["*.js", "README.md"],
14
+ "keywords": ["aap", "agent", "attestation", "verification", "middleware", "express", "ai"],
15
+ "author": "ira-hash",
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/ira-hash/agent-attestation-protocol.git",
20
+ "directory": "packages/server"
21
+ },
22
+ "homepage": "https://github.com/ira-hash/agent-attestation-protocol#readme",
23
+ "dependencies": {
24
+ "aap-agent-core": "^2.0.0"
25
+ },
26
+ "peerDependencies": {
27
+ "express": "^4.18.0 || ^5.0.0"
28
+ },
29
+ "peerDependenciesMeta": {
30
+ "express": {
31
+ "optional": true
32
+ }
33
+ },
34
+ "engines": {
35
+ "node": ">=18.0.0"
36
+ }
37
+ }