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 +177 -0
- package/challenges.js +525 -0
- package/errors.js +134 -0
- package/index.js +20 -0
- package/logger.js +139 -0
- package/middleware.js +375 -0
- package/package.json +37 -0
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
|
+
}
|