aap-agent-server 2.7.0 → 3.1.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/index.js CHANGED
@@ -1,46 +1,39 @@
1
1
  /**
2
- * @aap/server v2.7.0
2
+ * @aap/server v3.0.0
3
3
  *
4
- * Server-side utilities for Agent Attestation Protocol.
5
- * The Reverse Turing Test - CAPTCHAs block bots, AAP blocks humans.
4
+ * WebSocket-only Agent Attestation Protocol.
5
+ * Sequential challenges - no preview, no mercy.
6
6
  */
7
7
 
8
- export * from './middleware.js';
9
- export * from './challenges.js';
10
- export * from './ratelimit.js';
11
- export * from './whitelist.js';
12
- export * from './persistence.js';
13
- export * from './errors.js';
14
- export * from './websocket.js';
8
+ // Core WebSocket server
9
+ export { createAAPWebSocket } from './websocket.js';
10
+
11
+ // Challenge generation (internal use)
12
+ export {
13
+ generate,
14
+ generateBatch,
15
+ validateBatch,
16
+ getTypes,
17
+ BATCH_SIZE,
18
+ CHALLENGE_TYPES
19
+ } from './challenges.js';
20
+
21
+ // Optional utilities
22
+ export { createWhitelist, createKeyRotation } from './whitelist.js';
23
+ export { createStore, createMemoryStore, createFileStore, createRedisStore } from './persistence.js';
15
24
  export * as logger from './logger.js';
16
25
 
17
- import { aapMiddleware, createRouter } from './middleware.js';
18
- import challenges from './challenges.js';
19
- import { createRateLimiter, createFailureLimiter } from './ratelimit.js';
20
- import { createWhitelist, createKeyRotation } from './whitelist.js';
21
- import { createStore, createMemoryStore, createFileStore, createRedisStore } from './persistence.js';
22
- import { createAAPWebSocket } from './websocket.js';
26
+ // Constants
27
+ export const PROTOCOL_VERSION = '3.1.0';
28
+ export const TOTAL_TIME_MS = 6000;
29
+ export const CHALLENGE_COUNT = 7;
30
+ export const CONNECTION_TIMEOUT_MS = 60000;
23
31
 
24
- export { challenges };
32
+ import { createAAPWebSocket } from './websocket.js';
25
33
 
26
34
  export default {
27
- // Core
28
- aapMiddleware,
29
- createRouter,
30
- challenges,
31
-
32
- // WebSocket (v2.7+)
33
35
  createAAPWebSocket,
34
-
35
- // Security
36
- createRateLimiter,
37
- createFailureLimiter,
38
- createWhitelist,
39
- createKeyRotation,
40
-
41
- // Persistence
42
- createStore,
43
- createMemoryStore,
44
- createFileStore,
45
- createRedisStore
36
+ PROTOCOL_VERSION,
37
+ TOTAL_TIME_MS,
38
+ CHALLENGE_COUNT
46
39
  };
package/package.json CHANGED
@@ -1,17 +1,16 @@
1
1
  {
2
2
  "name": "aap-agent-server",
3
- "version": "2.7.0",
4
- "description": "Server middleware for Agent Attestation Protocol - verify AI agents",
3
+ "version": "3.1.0",
4
+ "description": "WebSocket server for Agent Attestation Protocol - verify AI agents",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
7
7
  "type": "module",
8
8
  "exports": {
9
9
  ".": "./index.js",
10
- "./middleware": "./middleware.js",
11
- "./challenges": "./challenges.js"
10
+ "./websocket": "./websocket.js"
12
11
  },
13
12
  "files": ["*.js", "README.md"],
14
- "keywords": ["aap", "agent", "attestation", "verification", "middleware", "express", "ai"],
13
+ "keywords": ["aap", "agent", "attestation", "verification", "websocket", "ai"],
15
14
  "author": "ira-hash",
16
15
  "license": "MIT",
17
16
  "repository": {
@@ -21,17 +20,8 @@
21
20
  },
22
21
  "homepage": "https://github.com/ira-hash/agent-attestation-protocol#readme",
23
22
  "dependencies": {
24
- "aap-agent-core": "^2.6.0",
25
23
  "ws": "^8.16.0"
26
24
  },
27
- "peerDependencies": {
28
- "express": "^4.18.0 || ^5.0.0"
29
- },
30
- "peerDependenciesMeta": {
31
- "express": {
32
- "optional": true
33
- }
34
- },
35
25
  "engines": {
36
26
  "node": ">=18.0.0"
37
27
  }
package/websocket.js CHANGED
@@ -1,282 +1,275 @@
1
1
  /**
2
- * AAP WebSocket Server v2.7
2
+ * AAP WebSocket Server v3.1
3
3
  *
4
- * Sequential challenge delivery over persistent connection.
5
- * Humans cannot preview questions - each arrives with strict time limit.
4
+ * Batch challenge delivery over WebSocket.
5
+ * All 7 challenges at once, 6 seconds total.
6
6
  */
7
7
 
8
8
  import { WebSocketServer } from 'ws';
9
- import { randomBytes, createVerify } from 'node:crypto';
10
- import { generate as generateChallenge, getTypes } from './challenges.js';
9
+ import { randomBytes, createHash } from 'node:crypto';
11
10
 
12
- // Constants
13
- const CHALLENGE_COUNT = 7;
14
- const TIME_PER_CHALLENGE_MS = 1200; // 1.2 seconds per challenge
15
- const CONNECTION_TIMEOUT_MS = 15000; // 15 seconds total
16
- const PROTOCOL_VERSION = '2.7.0';
11
+ // ============== CONSTANTS ==============
12
+ export const PROTOCOL_VERSION = '3.1.0';
13
+ export const CHALLENGE_COUNT = 7;
14
+ export const TOTAL_TIME_MS = 6000;
15
+ export const CONNECTION_TIMEOUT_MS = 60000;
16
+
17
+ // ============== CHALLENGE GENERATORS ==============
18
+ const GENERATORS = {
19
+ math: (salt, seed) => {
20
+ const a = 10 + (seed % 90);
21
+ const b = 10 + ((seed * 7) % 90);
22
+ const ops = ['+', '-', '*'];
23
+ const op = ops[seed % 3];
24
+ const answer = op === '+' ? a + b : op === '-' ? a - b : a * b;
25
+ return {
26
+ q: `[REQ-${salt}] What is ${a} ${op} ${b}?\nFormat: {"salt":"${salt}","result":number}`,
27
+ v: (r) => r?.salt === salt && r?.result === answer
28
+ };
29
+ },
30
+
31
+ logic: (salt, seed) => {
32
+ const x = 10 + (seed % 50);
33
+ const y = 10 + ((seed * 3) % 50);
34
+ const answer = x > y ? 'GREATER' : x < y ? 'LESS' : 'EQUAL';
35
+ return {
36
+ q: `[REQ-${salt}] X=${x}, Y=${y}. Answer "GREATER" if X>Y, "LESS" if X<Y, "EQUAL" if X=Y.\nFormat: {"salt":"${salt}","answer":"..."}`,
37
+ v: (r) => r?.salt === salt && r?.answer === answer
38
+ };
39
+ },
40
+
41
+ count: (salt, seed) => {
42
+ const all = ['cat','dog','apple','bird','car','fish','tree','book','lion','grape'];
43
+ const animals = ['cat','dog','bird','fish','lion'];
44
+ const n = 4 + (seed % 5);
45
+ const items = [];
46
+ for (let i = 0; i < n; i++) {
47
+ items.push(all[(seed + i * 3) % all.length]);
48
+ }
49
+ const count = items.filter(i => animals.includes(i)).length;
50
+ return {
51
+ q: `[REQ-${salt}] Count animals: ${items.join(', ')}\nFormat: {"salt":"${salt}","count":number}`,
52
+ v: (r) => r?.salt === salt && r?.count === count
53
+ };
54
+ },
55
+
56
+ pattern: (salt, seed) => {
57
+ const start = 2 + (seed % 10);
58
+ const step = 2 + (seed % 6);
59
+ const seq = [start, start+step, start+step*2, start+step*3];
60
+ const next = [start+step*4, start+step*5];
61
+ return {
62
+ q: `[REQ-${salt}] Next 2 numbers: [${seq.join(', ')}, ?, ?]\nFormat: {"salt":"${salt}","next":[n1,n2]}`,
63
+ v: (r) => r?.salt === salt && Array.isArray(r?.next) && r.next[0] === next[0] && r.next[1] === next[1]
64
+ };
65
+ },
66
+
67
+ reverse: (salt, seed) => {
68
+ const words = ['hello','world','agent','robot','claw','alpha','delta'];
69
+ const word = words[seed % words.length];
70
+ const rev = word.split('').reverse().join('');
71
+ return {
72
+ q: `[REQ-${salt}] Reverse the string: "${word}"\nFormat: {"salt":"${salt}","result":"..."}`,
73
+ v: (r) => r?.salt === salt && r?.result === rev
74
+ };
75
+ },
76
+
77
+ extract: (salt, seed) => {
78
+ const colors = ['red','blue','green','yellow','purple','orange'];
79
+ const color = colors[seed % colors.length];
80
+ const nouns = ['car','robot','bird','house'];
81
+ const noun = nouns[seed % nouns.length];
82
+ return {
83
+ q: `[REQ-${salt}] Extract the color: "The ${color} ${noun} moved quickly"\nFormat: {"salt":"${salt}","color":"..."}`,
84
+ v: (r) => r?.salt === salt && r?.color === color
85
+ };
86
+ },
87
+
88
+ longest: (salt, seed) => {
89
+ const sets = [
90
+ ['cat', 'elephant', 'dog', 'ant'],
91
+ ['bee', 'hippopotamus', 'fox', 'rat'],
92
+ ['owl', 'crocodile', 'bat', 'fly']
93
+ ];
94
+ const words = sets[seed % sets.length];
95
+ const longest = words.reduce((a, b) => a.length >= b.length ? a : b);
96
+ return {
97
+ q: `[REQ-${salt}] Find longest word: ${words.join(', ')}\nFormat: {"salt":"${salt}","answer":"..."}`,
98
+ v: (r) => r?.salt === salt && r?.answer === longest
99
+ };
100
+ }
101
+ };
102
+
103
+ const TYPES = Object.keys(GENERATORS);
104
+
105
+ function generateChallenge(nonce, index) {
106
+ const type = TYPES[index % TYPES.length];
107
+ const salt = createHash('sha256').update(nonce + index).digest('hex').slice(0, 6).toUpperCase();
108
+ const seed = parseInt(nonce.slice(index * 2, index * 2 + 8), 16) || (index * 17);
109
+ const { q, v } = GENERATORS[type](salt, seed);
110
+ return { id: index, type, challenge: q, validate: v };
111
+ }
112
+
113
+ // ============== WEBSOCKET SERVER ==============
17
114
 
18
115
  /**
19
- * Create AAP WebSocket server
20
- * @param {Object} options
21
- * @param {number} [options.port] - Standalone port (if no httpServer)
22
- * @param {Object} [options.httpServer] - Existing HTTP server
23
- * @param {string} [options.path='/aap'] - WebSocket path
24
- * @param {Function} [options.onVerified] - Callback on successful verification
25
- * @param {Function} [options.onFailed] - Callback on failed verification
116
+ * Create AAP WebSocket verification server
26
117
  */
27
118
  export function createAAPWebSocket(options = {}) {
28
119
  const {
29
120
  port,
30
- httpServer,
121
+ server,
31
122
  path = '/aap',
32
- onVerified = null,
33
- onFailed = null,
34
123
  challengeCount = CHALLENGE_COUNT,
35
- timePerChallengeMs = TIME_PER_CHALLENGE_MS
124
+ totalTimeMs = TOTAL_TIME_MS,
125
+ connectionTimeoutMs = CONNECTION_TIMEOUT_MS,
126
+ onVerified,
127
+ onFailed
36
128
  } = options;
37
129
 
38
- const wssOptions = httpServer
39
- ? { server: httpServer, path }
40
- : { port, path };
41
-
130
+ const wssOptions = server ? { server, path } : { port };
42
131
  const wss = new WebSocketServer(wssOptions);
43
-
44
- // Track active sessions
45
- const sessions = new Map();
132
+ const verifiedTokens = new Map();
46
133
 
47
- wss.on('connection', (ws, req) => {
134
+ wss.on('connection', (ws) => {
48
135
  const sessionId = randomBytes(16).toString('hex');
49
136
  const nonce = randomBytes(16).toString('hex');
50
- const startTime = Date.now();
51
-
52
- const session = {
53
- id: sessionId,
54
- nonce,
55
- ws,
56
- startTime,
57
- currentChallenge: 0,
58
- challenges: [],
59
- validators: [],
60
- answers: [],
61
- timings: [],
62
- publicKey: null,
63
- publicId: null,
64
- challengeStartTime: null,
65
- timeout: null,
66
- connectionTimeout: null
67
- };
68
-
69
- sessions.set(sessionId, session);
70
-
71
- // Connection timeout
72
- session.connectionTimeout = setTimeout(() => {
73
- sendError(ws, 'Connection timeout');
74
- ws.close();
75
- }, CONNECTION_TIMEOUT_MS);
76
-
77
- // Generate all challenges upfront (but send one at a time)
78
- const types = getTypes();
137
+ let challenges = [];
138
+ let validators = [];
139
+ let challengesSentAt = null;
140
+ let publicId = null;
141
+ let answered = false;
142
+
143
+ // Generate challenges
79
144
  for (let i = 0; i < challengeCount; i++) {
80
- const type = types[i % types.length];
81
- const challenge = generateChallenge(nonce + i, type);
82
- session.challenges.push({
83
- id: i,
84
- type,
85
- challenge_string: challenge.challenge_string
86
- });
87
- session.validators.push(challenge.validate);
145
+ const ch = generateChallenge(nonce, i);
146
+ challenges.push({ id: ch.id, type: ch.type, challenge: ch.challenge });
147
+ validators.push(ch.validate);
88
148
  }
89
-
149
+
150
+ // Connection timeout
151
+ const connTimer = setTimeout(() => {
152
+ send(ws, { type: 'error', code: 'TIMEOUT', message: 'Connection timeout' });
153
+ ws.close();
154
+ }, connectionTimeoutMs);
155
+
90
156
  // Send handshake
91
157
  send(ws, {
92
158
  type: 'handshake',
93
159
  sessionId,
94
- nonce,
95
160
  protocol: 'AAP',
96
161
  version: PROTOCOL_VERSION,
97
- mode: 'websocket',
162
+ mode: 'batch',
98
163
  challengeCount,
99
- timePerChallengeMs,
100
- message: 'Connected. Send "ready" with publicKey to begin.'
164
+ totalTimeMs,
165
+ message: 'Send {"type":"ready"} to receive challenges.'
101
166
  });
102
-
167
+
103
168
  ws.on('message', (data) => {
169
+ let msg;
104
170
  try {
105
- const msg = JSON.parse(data.toString());
106
- handleMessage(session, msg, { onVerified, onFailed, timePerChallengeMs });
107
- } catch (e) {
108
- sendError(ws, 'Invalid JSON');
109
- }
110
- });
111
-
112
- ws.on('close', () => {
113
- clearTimeout(session.timeout);
114
- clearTimeout(session.connectionTimeout);
115
- sessions.delete(sessionId);
116
- });
117
-
118
- ws.on('error', () => {
119
- sessions.delete(sessionId);
120
- });
121
- });
122
-
123
- return {
124
- wss,
125
- sessions,
126
- close: () => wss.close()
127
- };
128
- }
129
-
130
- function send(ws, data) {
131
- if (ws.readyState === 1) { // OPEN
132
- ws.send(JSON.stringify(data));
133
- }
134
- }
135
-
136
- function sendError(ws, message, code = 'ERROR') {
137
- send(ws, { type: 'error', code, message });
138
- }
139
-
140
- function handleMessage(session, msg, options) {
141
- const { ws, currentChallenge, challenges, validators, answers, timings } = session;
142
- const { onVerified, onFailed, timePerChallengeMs } = options;
143
-
144
- switch (msg.type) {
145
- case 'ready':
146
- // Client ready to start, optionally provides identity
147
- if (currentChallenge !== 0) {
148
- sendError(ws, 'Already started');
149
- return;
150
- }
151
-
152
- session.publicKey = msg.publicKey || null;
153
- session.publicId = msg.publicId || null;
154
-
155
- // Send first challenge
156
- sendNextChallenge(session, timePerChallengeMs);
157
- break;
158
-
159
- case 'answer':
160
- // Client submitting answer
161
- if (session.challengeStartTime === null) {
162
- sendError(ws, 'No active challenge');
171
+ msg = JSON.parse(data.toString());
172
+ } catch {
173
+ send(ws, { type: 'error', code: 'INVALID_JSON', message: 'Invalid JSON' });
163
174
  return;
164
175
  }
165
-
166
- const responseTime = Date.now() - session.challengeStartTime;
167
- clearTimeout(session.timeout);
168
-
169
- // Check if too slow
170
- if (responseTime > timePerChallengeMs) {
176
+
177
+ if (msg.type === 'ready' && !challengesSentAt) {
178
+ publicId = msg.publicId || 'anon-' + randomBytes(4).toString('hex');
179
+ challengesSentAt = Date.now();
180
+
181
+ // Send all challenges at once
171
182
  send(ws, {
172
- type: 'timeout',
173
- challengeId: currentChallenge,
174
- responseTimeMs: responseTime,
175
- limit: timePerChallengeMs
183
+ type: 'challenges',
184
+ nonce,
185
+ challenges,
186
+ totalTimeMs,
187
+ expiresAt: challengesSentAt + totalTimeMs
176
188
  });
177
- finishSession(session, false, 'Too slow on challenge ' + currentChallenge, { onVerified, onFailed });
178
- return;
179
189
  }
180
-
181
- // Record answer and timing
182
- answers.push(msg.answer);
183
- timings.push(responseTime);
184
-
185
- // Validate immediately
186
- const valid = validators[currentChallenge](msg.answer);
187
-
188
- send(ws, {
189
- type: 'ack',
190
- challengeId: currentChallenge,
191
- responseTimeMs: responseTime,
192
- valid // Real-time feedback
193
- });
194
-
195
- session.currentChallenge++;
196
-
197
- // More challenges?
198
- if (session.currentChallenge < challenges.length) {
199
- // Small delay then next challenge
200
- setTimeout(() => sendNextChallenge(session, timePerChallengeMs), 100);
201
- } else {
202
- // All done - calculate result
203
- const passed = answers.filter((_, i) => validators[i](answers[i])).length;
204
- const allPassed = passed === challenges.length;
205
- const totalTime = Date.now() - session.startTime;
190
+ else if (msg.type === 'answers' && challengesSentAt && !answered) {
191
+ answered = true;
192
+ clearTimeout(connTimer);
193
+
194
+ const elapsed = Date.now() - challengesSentAt;
195
+ const answers = msg.answers || [];
206
196
 
207
- finishSession(session, allPassed, allPassed ? 'Verified' : `Failed: ${passed}/${challenges.length}`, {
208
- onVerified,
209
- onFailed,
197
+ // Too slow?
198
+ if (elapsed > totalTimeMs) {
199
+ const result = {
200
+ type: 'result',
201
+ verified: false,
202
+ message: `Too slow: ${elapsed}ms > ${totalTimeMs}ms`,
203
+ publicId,
204
+ responseTimeMs: elapsed
205
+ };
206
+ if (onFailed) onFailed(result);
207
+ send(ws, result);
208
+ setTimeout(() => ws.close(), 300);
209
+ return;
210
+ }
211
+
212
+ // Validate all answers
213
+ let passed = 0;
214
+ const results = [];
215
+ for (let i = 0; i < challengeCount; i++) {
216
+ const valid = validators[i](answers[i]);
217
+ if (valid) passed++;
218
+ results.push({ id: i, valid });
219
+ }
220
+
221
+ const success = passed === challengeCount;
222
+ const result = {
223
+ type: 'result',
224
+ verified: success,
225
+ message: success ? 'All challenges passed' : `Failed: ${passed}/${challengeCount}`,
226
+ publicId,
210
227
  passed,
211
- total: challenges.length,
212
- timings,
213
- totalTime
214
- });
228
+ total: challengeCount,
229
+ results,
230
+ responseTimeMs: elapsed
231
+ };
232
+
233
+ if (success) {
234
+ result.role = 'AI_AGENT';
235
+ result.sessionToken = randomBytes(32).toString('hex');
236
+
237
+ verifiedTokens.set(result.sessionToken, {
238
+ publicId,
239
+ nonce,
240
+ verifiedAt: Date.now(),
241
+ expiresAt: Date.now() + 3600000,
242
+ responseTimeMs: elapsed
243
+ });
244
+
245
+ if (onVerified) onVerified(result);
246
+ } else {
247
+ if (onFailed) onFailed(result);
248
+ }
249
+
250
+ send(ws, result);
251
+ setTimeout(() => ws.close(), 300);
215
252
  }
216
- break;
217
-
218
- default:
219
- sendError(ws, 'Unknown message type: ' + msg.type);
220
- }
221
- }
253
+ });
222
254
 
223
- function sendNextChallenge(session, timePerChallengeMs) {
224
- const { ws, currentChallenge, challenges } = session;
225
- const challenge = challenges[currentChallenge];
226
-
227
- session.challengeStartTime = Date.now();
228
-
229
- send(ws, {
230
- type: 'challenge',
231
- id: currentChallenge,
232
- total: challenges.length,
233
- challenge: challenge.challenge_string,
234
- timeLimit: timePerChallengeMs
255
+ ws.on('close', () => clearTimeout(connTimer));
256
+ ws.on('error', () => clearTimeout(connTimer));
235
257
  });
236
-
237
- // Timeout for this challenge
238
- session.timeout = setTimeout(() => {
239
- send(ws, {
240
- type: 'timeout',
241
- challengeId: currentChallenge,
242
- message: 'Time expired'
243
- });
244
- finishSession(session, false, 'Timeout on challenge ' + currentChallenge, {});
245
- }, timePerChallengeMs + 100); // Small grace period for network
246
- }
247
258
 
248
- function finishSession(session, success, message, options) {
249
- const { ws, nonce, publicId, startTime } = session;
250
- const { onVerified, onFailed, passed, total, timings, totalTime } = options;
251
-
252
- clearTimeout(session.timeout);
253
- clearTimeout(session.connectionTimeout);
254
-
255
- const result = {
256
- type: 'result',
257
- verified: success,
258
- nonce,
259
- publicId,
260
- message,
261
- passed,
262
- total,
263
- timings,
264
- totalTimeMs: totalTime || (Date.now() - startTime),
265
- avgResponseMs: timings?.length ? Math.round(timings.reduce((a, b) => a + b, 0) / timings.length) : null
259
+ return {
260
+ wss,
261
+ verifiedTokens,
262
+ close: () => wss.close(),
263
+ isVerified: (token) => {
264
+ const session = verifiedTokens.get(token);
265
+ return session && Date.now() < session.expiresAt;
266
+ },
267
+ getSession: (token) => verifiedTokens.get(token)
266
268
  };
267
-
268
- if (success) {
269
- result.role = 'AI_AGENT';
270
- result.sessionToken = randomBytes(32).toString('hex');
271
- if (onVerified) onVerified(result, session);
272
- } else {
273
- if (onFailed) onFailed(result, session);
274
- }
275
-
276
- send(ws, result);
277
-
278
- // Close after sending result
279
- setTimeout(() => ws.close(), 500);
280
269
  }
281
270
 
282
- export default { createAAPWebSocket };
271
+ function send(ws, data) {
272
+ if (ws.readyState === 1) ws.send(JSON.stringify(data));
273
+ }
274
+
275
+ export default { createAAPWebSocket, PROTOCOL_VERSION, CHALLENGE_COUNT, TOTAL_TIME_MS };
package/errors.js DELETED
@@ -1,134 +0,0 @@
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/middleware.js DELETED
@@ -1,420 +0,0 @@
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
- import * as logger from './logger.js';
19
- import { ErrorCodes, sendError } from './errors.js';
20
-
21
- /**
22
- * Create AAP verification middleware/router
23
- *
24
- * @param {Object} [options]
25
- * @param {number} [options.challengeExpiryMs=60000] - Challenge expiration time
26
- * @param {number} [options.maxResponseTimeMs=8000] - Max response time for batch
27
- * @param {number} [options.batchSize=5] - Number of challenges per batch
28
- * @param {number} [options.minPassCount] - Minimum challenges to pass (default: all)
29
- * @param {Function} [options.onVerified] - Callback when agent is verified
30
- * @param {Function} [options.onFailed] - Callback when verification fails
31
- * @returns {Function} Express router
32
- */
33
- export function aapMiddleware(options = {}) {
34
- const {
35
- challengeExpiryMs = CHALLENGE_EXPIRY_MS,
36
- maxResponseTimeMs = MAX_RESPONSE_TIME_MS,
37
- batchSize = BATCH_SIZE,
38
- minPassCount = null, // null = all must pass
39
- onVerified,
40
- onFailed
41
- } = options;
42
-
43
- // In-memory challenge store with size limit (DoS protection)
44
- const MAX_CHALLENGES = 10000;
45
- const challenges = new Map();
46
-
47
- // Cleanup expired challenges periodically
48
- const cleanup = () => {
49
- const now = Date.now();
50
- for (const [nonce, challenge] of challenges.entries()) {
51
- if (now > challenge.expiresAt) {
52
- challenges.delete(nonce);
53
- }
54
- }
55
-
56
- // Emergency cleanup if still too many (keep newest)
57
- if (challenges.size > MAX_CHALLENGES) {
58
- const entries = [...challenges.entries()]
59
- .sort((a, b) => b[1].timestamp - a[1].timestamp)
60
- .slice(0, MAX_CHALLENGES / 2);
61
- challenges.clear();
62
- entries.forEach(([k, v]) => challenges.set(k, v));
63
- }
64
- };
65
-
66
- // Return a function that creates routes
67
- return (router) => {
68
- /**
69
- * GET /health - Health check
70
- */
71
- router.get('/health', (req, res) => {
72
- res.json({
73
- status: 'ok',
74
- protocol: 'AAP',
75
- version: '2.0.0',
76
- mode: 'batch',
77
- batchSize,
78
- maxResponseTimeMs,
79
- challengeTypes: getTypes(),
80
- activeChallenges: challenges.size
81
- });
82
- });
83
-
84
- /**
85
- * POST /challenge - Request a batch of challenges
86
- */
87
- router.post('/challenge', (req, res) => {
88
- cleanup();
89
-
90
- const nonce = generateNonce();
91
- const timestamp = Date.now();
92
- const { challenges: batchChallenges, validators, expected } = generateBatch(nonce, batchSize);
93
-
94
- const challengeData = {
95
- nonce,
96
- challenges: batchChallenges,
97
- batchSize,
98
- timestamp,
99
- expiresAt: timestamp + challengeExpiryMs,
100
- maxResponseTimeMs
101
- };
102
-
103
- // Store with validators (not sent to client)
104
- challenges.set(nonce, {
105
- ...challengeData,
106
- validators,
107
- expected // For debugging
108
- });
109
-
110
- // Send without validators
111
- res.json({
112
- nonce,
113
- challenges: batchChallenges,
114
- batchSize,
115
- timestamp,
116
- expiresAt: challengeData.expiresAt,
117
- maxResponseTimeMs
118
- });
119
- });
120
-
121
- /**
122
- * POST /verify - Verify agent's batch solutions
123
- */
124
- router.post('/verify', (req, res) => {
125
- const {
126
- solutions,
127
- signature,
128
- publicKey,
129
- publicId,
130
- nonce,
131
- timestamp,
132
- responseTimeMs
133
- } = req.body;
134
-
135
- const checks = {
136
- inputValid: false,
137
- challengeExists: false,
138
- notExpired: false,
139
- solutionsExist: false,
140
- solutionsValid: false,
141
- responseTimeValid: false,
142
- signatureValid: false
143
- };
144
-
145
- try {
146
- // Check 0: Input validation (security)
147
- if (!nonce || typeof nonce !== 'string' || nonce.length !== 32) {
148
- return res.status(400).json({ verified: false, error: 'Invalid nonce format', checks });
149
- }
150
- if (!publicId || typeof publicId !== 'string' || publicId.length !== 20) {
151
- return res.status(400).json({ verified: false, error: 'Invalid publicId format', checks });
152
- }
153
- if (!signature || typeof signature !== 'string' || signature.length < 50) {
154
- return res.status(400).json({ verified: false, error: 'Invalid signature format', checks });
155
- }
156
- if (!publicKey || typeof publicKey !== 'string' || !publicKey.includes('BEGIN PUBLIC KEY')) {
157
- return res.status(400).json({ verified: false, error: 'Invalid publicKey format', checks });
158
- }
159
- if (!timestamp || typeof timestamp !== 'number') {
160
- return res.status(400).json({ verified: false, error: 'Invalid timestamp', checks });
161
- }
162
- if (!responseTimeMs || typeof responseTimeMs !== 'number' || responseTimeMs < 0) {
163
- return res.status(400).json({ verified: false, error: 'Invalid responseTimeMs', checks });
164
- }
165
- checks.inputValid = true;
166
-
167
- // Check 1: Challenge exists (check BEFORE delete for race condition fix)
168
- const challenge = challenges.get(nonce);
169
- if (!challenge) {
170
- if (onFailed) onFailed({ error: 'Challenge not found', checks }, req);
171
- return res.status(400).json({
172
- verified: false,
173
- error: 'Challenge not found or already used',
174
- checks
175
- });
176
- }
177
- checks.challengeExists = true;
178
-
179
- // Check 2: Not expired (check BEFORE delete - race condition fix)
180
- if (Date.now() > challenge.expiresAt) {
181
- challenges.delete(nonce); // Clean up expired
182
- if (onFailed) onFailed({ error: 'Challenge expired', checks }, req);
183
- return res.status(400).json({
184
- verified: false,
185
- error: 'Challenge expired',
186
- checks
187
- });
188
- }
189
- checks.notExpired = true;
190
-
191
- // Remove challenge (one-time use) - only after expiry check
192
- const { validators, batchSize: size } = challenge;
193
- challenges.delete(nonce);
194
-
195
- // Check 3: Solutions exist
196
- if (!solutions || !Array.isArray(solutions) || solutions.length !== size) {
197
- if (onFailed) onFailed({ error: 'Invalid solutions array', checks }, req);
198
- return res.status(400).json({
199
- verified: false,
200
- error: `Expected ${size} solutions, got ${solutions?.length || 0}`,
201
- checks
202
- });
203
- }
204
- checks.solutionsExist = true;
205
-
206
- // Check 4: Validate all solutions (Proof of Intelligence)
207
- const batchResult = validateBatch(validators, solutions);
208
- const requiredPass = minPassCount || size;
209
-
210
- if (batchResult.passed < requiredPass) {
211
- if (onFailed) onFailed({ error: 'Solutions validation failed', checks, batchResult }, req);
212
- return res.status(400).json({
213
- verified: false,
214
- error: `Proof of Intelligence failed: ${batchResult.passed}/${size} correct (need ${requiredPass})`,
215
- checks,
216
- batchResult
217
- });
218
- }
219
- checks.solutionsValid = true;
220
-
221
- // Check 5: Response time (Proof of Liveness) - SERVER-SIDE validation
222
- const serverResponseTime = Date.now() - challenge.timestamp;
223
- const effectiveResponseTime = Math.max(responseTimeMs, serverResponseTime);
224
-
225
- if (effectiveResponseTime > maxResponseTimeMs) {
226
- if (onFailed) onFailed({ error: 'Response too slow', checks }, req);
227
- return res.status(400).json({
228
- verified: false,
229
- error: `Response too slow: ${effectiveResponseTime}ms > ${maxResponseTimeMs}ms (Proof of Liveness failed)`,
230
- checks,
231
- timing: { client: responseTimeMs, server: serverResponseTime }
232
- });
233
- }
234
- checks.responseTimeValid = true;
235
-
236
- // Check 6: Signature (Proof of Identity)
237
- // Sign over solutions array
238
- const solutionsString = JSON.stringify(solutions);
239
- const proofData = createProofData({ nonce, solution: solutionsString, publicId, timestamp });
240
- if (!verify(proofData, signature, publicKey)) {
241
- if (onFailed) onFailed({ error: 'Invalid signature', checks }, req);
242
- return res.status(400).json({
243
- verified: false,
244
- error: 'Invalid signature (Proof of Identity failed)',
245
- checks
246
- });
247
- }
248
- checks.signatureValid = true;
249
-
250
- // All checks passed
251
- const result = {
252
- verified: true,
253
- role: 'AI_AGENT',
254
- publicId,
255
- batchResult,
256
- responseTimeMs,
257
- checks
258
- };
259
-
260
- if (onVerified) onVerified(result, req);
261
-
262
- res.json(result);
263
-
264
- } catch (error) {
265
- if (onFailed) onFailed({ error: error.message, checks }, req);
266
- res.status(500).json({
267
- verified: false,
268
- error: `Verification error: ${error.message}`,
269
- checks
270
- });
271
- }
272
- });
273
-
274
- // ============== Legacy single-challenge endpoints ==============
275
-
276
- /**
277
- * POST /challenge/single - Request a single challenge (legacy)
278
- */
279
- router.post('/challenge/single', (req, res) => {
280
- cleanup();
281
-
282
- const nonce = generateNonce();
283
- const timestamp = Date.now();
284
- const { type, challenge_string, validate } = generateChallenge(nonce);
285
-
286
- const challenge = {
287
- challenge_string,
288
- nonce,
289
- type,
290
- difficulty: 1,
291
- timestamp,
292
- expiresAt: timestamp + challengeExpiryMs,
293
- mode: 'single'
294
- };
295
-
296
- challenges.set(nonce, { ...challenge, validate });
297
-
298
- res.json({
299
- challenge_string,
300
- nonce,
301
- type,
302
- difficulty: 1,
303
- timestamp,
304
- expiresAt: challenge.expiresAt,
305
- mode: 'single',
306
- maxResponseTimeMs: 10000 // 10s for single
307
- });
308
- });
309
-
310
- /**
311
- * POST /verify/single - Verify single challenge (legacy)
312
- */
313
- router.post('/verify/single', (req, res) => {
314
- const {
315
- solution,
316
- signature,
317
- publicKey,
318
- publicId,
319
- nonce,
320
- timestamp,
321
- responseTimeMs
322
- } = req.body;
323
-
324
- const checks = {
325
- challengeExists: false,
326
- notExpired: false,
327
- solutionExists: false,
328
- solutionValid: false,
329
- responseTimeValid: false,
330
- signatureValid: false
331
- };
332
-
333
- try {
334
- const challenge = challenges.get(nonce);
335
- if (!challenge || challenge.mode !== 'single') {
336
- return res.status(400).json({
337
- verified: false,
338
- error: 'Single challenge not found',
339
- checks
340
- });
341
- }
342
- checks.challengeExists = true;
343
-
344
- const { validate, type: challengeType } = challenge;
345
- challenges.delete(nonce);
346
-
347
- if (Date.now() > challenge.expiresAt) {
348
- return res.status(400).json({ verified: false, error: 'Challenge expired', checks });
349
- }
350
- checks.notExpired = true;
351
-
352
- if (!solution) {
353
- return res.status(400).json({ verified: false, error: 'Missing solution', checks });
354
- }
355
- checks.solutionExists = true;
356
-
357
- if (!validate(solution)) {
358
- return res.status(400).json({ verified: false, error: 'Invalid solution', checks });
359
- }
360
- checks.solutionValid = true;
361
-
362
- if (responseTimeMs > 10000) {
363
- return res.status(400).json({ verified: false, error: 'Too slow', checks });
364
- }
365
- checks.responseTimeValid = true;
366
-
367
- const proofData = createProofData({ nonce, solution, publicId, timestamp });
368
- if (!verify(proofData, signature, publicKey)) {
369
- return res.status(400).json({ verified: false, error: 'Invalid signature', checks });
370
- }
371
- checks.signatureValid = true;
372
-
373
- res.json({
374
- verified: true,
375
- role: 'AI_AGENT',
376
- publicId,
377
- challengeType,
378
- checks
379
- });
380
-
381
- } catch (error) {
382
- res.status(500).json({ verified: false, error: error.message, checks });
383
- }
384
- });
385
-
386
- return router;
387
- };
388
- }
389
-
390
- /**
391
- * Create a standalone AAP router for Express
392
- *
393
- * @param {Object} [options] - Middleware options
394
- * @returns {Router} Express router
395
- *
396
- * @example
397
- * import express from 'express';
398
- * import { createRouter } from '@aap/server';
399
- *
400
- * const app = express();
401
- * app.use('/aap/v1', createRouter());
402
- */
403
- export async function createRouter(options = {}) {
404
- // Dynamic import for optional express dependency
405
- let express;
406
- try {
407
- express = (await import('express')).default;
408
- } catch {
409
- throw new Error('express is required for createRouter. Install with: npm install express');
410
- }
411
- const router = express.Router();
412
- router.use(express.json());
413
-
414
- const middleware = aapMiddleware(options);
415
- middleware(router);
416
-
417
- return router;
418
- }
419
-
420
- export default { aapMiddleware, createRouter };
package/ratelimit.js DELETED
@@ -1,117 +0,0 @@
1
- /**
2
- * AAP Rate Limiter
3
- *
4
- * Simple in-memory rate limiting (no external dependencies)
5
- */
6
-
7
- /**
8
- * Create a rate limiter
9
- * @param {Object} options
10
- * @param {number} [options.windowMs=60000] - Time window in ms
11
- * @param {number} [options.max=10] - Max requests per window
12
- * @param {string} [options.message] - Error message
13
- * @returns {Function} Express middleware
14
- */
15
- export function createRateLimiter(options = {}) {
16
- const {
17
- windowMs = 60000,
18
- max = 10,
19
- message = 'Too many requests, please try again later'
20
- } = options;
21
-
22
- const requests = new Map();
23
-
24
- // Cleanup old entries periodically
25
- setInterval(() => {
26
- const now = Date.now();
27
- for (const [key, data] of requests.entries()) {
28
- if (now - data.firstRequest > windowMs) {
29
- requests.delete(key);
30
- }
31
- }
32
- }, windowMs);
33
-
34
- return (req, res, next) => {
35
- const key = req.ip || req.connection.remoteAddress || 'unknown';
36
- const now = Date.now();
37
-
38
- let data = requests.get(key);
39
-
40
- if (!data || now - data.firstRequest > windowMs) {
41
- // New window
42
- data = { count: 1, firstRequest: now };
43
- requests.set(key, data);
44
- } else {
45
- data.count++;
46
- }
47
-
48
- // Set headers
49
- const remaining = Math.max(0, max - data.count);
50
- const resetTime = Math.ceil((data.firstRequest + windowMs) / 1000);
51
-
52
- res.setHeader('X-RateLimit-Limit', max);
53
- res.setHeader('X-RateLimit-Remaining', remaining);
54
- res.setHeader('X-RateLimit-Reset', resetTime);
55
-
56
- if (data.count > max) {
57
- res.setHeader('Retry-After', Math.ceil((data.firstRequest + windowMs - now) / 1000));
58
- return res.status(429).json({
59
- error: message,
60
- retryAfter: Math.ceil((data.firstRequest + windowMs - now) / 1000)
61
- });
62
- }
63
-
64
- next();
65
- };
66
- }
67
-
68
- /**
69
- * Create rate limiter for failed attempts
70
- * Stricter limits after failures
71
- */
72
- export function createFailureLimiter(options = {}) {
73
- const {
74
- windowMs = 60000,
75
- maxFailures = 5,
76
- message = 'Too many failed attempts'
77
- } = options;
78
-
79
- const failures = new Map();
80
-
81
- return {
82
- middleware: (req, res, next) => {
83
- const key = req.ip || req.connection.remoteAddress || 'unknown';
84
- const data = failures.get(key);
85
-
86
- if (data && data.count >= maxFailures && Date.now() - data.firstFailure < windowMs) {
87
- return res.status(429).json({
88
- error: message,
89
- retryAfter: Math.ceil((data.firstFailure + windowMs - Date.now()) / 1000)
90
- });
91
- }
92
-
93
- next();
94
- },
95
-
96
- recordFailure: (req) => {
97
- const key = req.ip || req.connection.remoteAddress || 'unknown';
98
- const now = Date.now();
99
- let data = failures.get(key);
100
-
101
- if (!data || now - data.firstFailure > windowMs) {
102
- data = { count: 1, firstFailure: now };
103
- } else {
104
- data.count++;
105
- }
106
-
107
- failures.set(key, data);
108
- },
109
-
110
- clearFailures: (req) => {
111
- const key = req.ip || req.connection.remoteAddress || 'unknown';
112
- failures.delete(key);
113
- }
114
- };
115
- }
116
-
117
- export default { createRateLimiter, createFailureLimiter };