aap-agent-server 2.7.0 → 3.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/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.0.0';
28
+ export const TIME_PER_CHALLENGE_MS = 1200;
29
+ export const CHALLENGE_COUNT = 7;
30
+ export const CONNECTION_TIMEOUT_MS = 15000;
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
+ TIME_PER_CHALLENGE_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.0.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,121 +1,215 @@
1
1
  /**
2
- * AAP WebSocket Server v2.7
2
+ * AAP WebSocket Server v3.0
3
3
  *
4
4
  * Sequential challenge delivery over persistent connection.
5
- * Humans cannot preview questions - each arrives with strict time limit.
5
+ * No preview. Server controls pacing. Humans cannot pass.
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, createVerify } 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.0.0';
13
+ export const CHALLENGE_COUNT = 7;
14
+ export const TIME_PER_CHALLENGE_MS = 1200;
15
+ export const CONNECTION_TIMEOUT_MS = 15000;
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 { type, challenge: q, validate: v };
111
+ }
112
+
113
+ // ============== WEBSOCKET SERVER ==============
17
114
 
18
115
  /**
19
- * Create AAP WebSocket server
116
+ * Create AAP WebSocket verification server
20
117
  * @param {Object} options
21
- * @param {number} [options.port] - Standalone port (if no httpServer)
22
- * @param {Object} [options.httpServer] - Existing HTTP server
118
+ * @param {number} [options.port] - Port for standalone server
119
+ * @param {Object} [options.server] - Existing HTTP server to attach to
23
120
  * @param {string} [options.path='/aap'] - WebSocket path
121
+ * @param {number} [options.challengeCount=7] - Number of challenges
122
+ * @param {number} [options.timePerChallengeMs=1200] - Time limit per challenge
24
123
  * @param {Function} [options.onVerified] - Callback on successful verification
25
124
  * @param {Function} [options.onFailed] - Callback on failed verification
125
+ * @returns {Object} { wss, sessions, close }
26
126
  */
27
127
  export function createAAPWebSocket(options = {}) {
28
128
  const {
29
129
  port,
30
- httpServer,
130
+ server,
31
131
  path = '/aap',
32
- onVerified = null,
33
- onFailed = null,
34
132
  challengeCount = CHALLENGE_COUNT,
35
- timePerChallengeMs = TIME_PER_CHALLENGE_MS
133
+ timePerChallengeMs = TIME_PER_CHALLENGE_MS,
134
+ connectionTimeoutMs = CONNECTION_TIMEOUT_MS,
135
+ onVerified,
136
+ onFailed
36
137
  } = options;
37
138
 
38
- const wssOptions = httpServer
39
- ? { server: httpServer, path }
40
- : { port, path };
41
-
139
+ const wssOptions = server ? { server, path } : { port };
42
140
  const wss = new WebSocketServer(wssOptions);
43
-
44
- // Track active sessions
45
141
  const sessions = new Map();
142
+ const verifiedTokens = new Map();
46
143
 
47
- wss.on('connection', (ws, req) => {
144
+ wss.on('connection', (ws) => {
48
145
  const sessionId = randomBytes(16).toString('hex');
49
146
  const nonce = randomBytes(16).toString('hex');
50
147
  const startTime = Date.now();
51
-
148
+
52
149
  const session = {
53
150
  id: sessionId,
54
- nonce,
55
151
  ws,
152
+ nonce,
56
153
  startTime,
57
- currentChallenge: 0,
154
+ current: 0,
58
155
  challenges: [],
59
- validators: [],
60
156
  answers: [],
61
157
  timings: [],
62
- publicKey: null,
63
158
  publicId: null,
64
- challengeStartTime: null,
65
- timeout: null,
66
- connectionTimeout: null
159
+ challengeStart: null,
160
+ timer: null,
161
+ connTimer: null
67
162
  };
68
-
163
+
69
164
  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();
165
+
166
+ // Generate challenges
79
167
  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);
168
+ session.challenges.push(generateChallenge(nonce, i));
88
169
  }
89
-
170
+
171
+ // Connection timeout
172
+ session.connTimer = setTimeout(() => {
173
+ send(ws, { type: 'error', code: 'TIMEOUT', message: 'Connection timeout' });
174
+ ws.close();
175
+ }, connectionTimeoutMs);
176
+
90
177
  // Send handshake
91
178
  send(ws, {
92
179
  type: 'handshake',
93
180
  sessionId,
94
- nonce,
95
181
  protocol: 'AAP',
96
182
  version: PROTOCOL_VERSION,
97
- mode: 'websocket',
98
183
  challengeCount,
99
184
  timePerChallengeMs,
100
- message: 'Connected. Send "ready" with publicKey to begin.'
185
+ message: 'Send {"type":"ready"} to begin verification.'
101
186
  });
102
-
187
+
103
188
  ws.on('message', (data) => {
189
+ let msg;
104
190
  try {
105
- const msg = JSON.parse(data.toString());
106
- handleMessage(session, msg, { onVerified, onFailed, timePerChallengeMs });
107
- } catch (e) {
108
- sendError(ws, 'Invalid JSON');
191
+ msg = JSON.parse(data.toString());
192
+ } catch {
193
+ send(ws, { type: 'error', code: 'INVALID_JSON', message: 'Invalid JSON' });
194
+ return;
109
195
  }
196
+
197
+ handleMessage(session, msg, {
198
+ challengeCount,
199
+ timePerChallengeMs,
200
+ onVerified,
201
+ onFailed,
202
+ verifiedTokens
203
+ });
110
204
  });
111
-
205
+
112
206
  ws.on('close', () => {
113
- clearTimeout(session.timeout);
114
- clearTimeout(session.connectionTimeout);
207
+ cleanup(session);
115
208
  sessions.delete(sessionId);
116
209
  });
117
-
210
+
118
211
  ws.on('error', () => {
212
+ cleanup(session);
119
213
  sessions.delete(sessionId);
120
214
  });
121
215
  });
@@ -123,160 +217,150 @@ export function createAAPWebSocket(options = {}) {
123
217
  return {
124
218
  wss,
125
219
  sessions,
126
- close: () => wss.close()
220
+ verifiedTokens,
221
+ close: () => wss.close(),
222
+
223
+ // Helper to check if token is verified
224
+ isVerified: (token) => {
225
+ const session = verifiedTokens.get(token);
226
+ return session && Date.now() < session.expiresAt;
227
+ },
228
+
229
+ // Get verified session info
230
+ getSession: (token) => verifiedTokens.get(token)
127
231
  };
128
232
  }
129
233
 
130
234
  function send(ws, data) {
131
- if (ws.readyState === 1) { // OPEN
235
+ if (ws.readyState === 1) {
132
236
  ws.send(JSON.stringify(data));
133
237
  }
134
238
  }
135
239
 
136
- function sendError(ws, message, code = 'ERROR') {
137
- send(ws, { type: 'error', code, message });
240
+ function cleanup(session) {
241
+ clearTimeout(session.timer);
242
+ clearTimeout(session.connTimer);
138
243
  }
139
244
 
140
245
  function handleMessage(session, msg, options) {
141
- const { ws, currentChallenge, challenges, validators, answers, timings } = session;
142
- const { onVerified, onFailed, timePerChallengeMs } = options;
246
+ const { ws, current, challenges, answers, timings } = session;
247
+ const { challengeCount, timePerChallengeMs, onVerified, onFailed, verifiedTokens } = options;
143
248
 
144
249
  switch (msg.type) {
145
250
  case 'ready':
146
- // Client ready to start, optionally provides identity
147
- if (currentChallenge !== 0) {
148
- sendError(ws, 'Already started');
251
+ if (current !== 0) {
252
+ send(ws, { type: 'error', code: 'ALREADY_STARTED', message: 'Already started' });
149
253
  return;
150
254
  }
151
-
152
- session.publicKey = msg.publicKey || null;
153
- session.publicId = msg.publicId || null;
154
-
155
- // Send first challenge
255
+ session.publicId = msg.publicId || 'anon-' + randomBytes(4).toString('hex');
156
256
  sendNextChallenge(session, timePerChallengeMs);
157
257
  break;
158
-
258
+
159
259
  case 'answer':
160
- // Client submitting answer
161
- if (session.challengeStartTime === null) {
162
- sendError(ws, 'No active challenge');
260
+ if (session.challengeStart === null) {
261
+ send(ws, { type: 'error', code: 'NO_CHALLENGE', message: 'No active challenge' });
163
262
  return;
164
263
  }
165
-
166
- const responseTime = Date.now() - session.challengeStartTime;
167
- clearTimeout(session.timeout);
168
-
169
- // Check if too slow
170
- if (responseTime > timePerChallengeMs) {
171
- send(ws, {
172
- type: 'timeout',
173
- challengeId: currentChallenge,
174
- responseTimeMs: responseTime,
175
- limit: timePerChallengeMs
176
- });
177
- finishSession(session, false, 'Too slow on challenge ' + currentChallenge, { onVerified, onFailed });
264
+
265
+ clearTimeout(session.timer);
266
+ const elapsed = Date.now() - session.challengeStart;
267
+
268
+ // Too slow?
269
+ if (elapsed > timePerChallengeMs) {
270
+ finishSession(session, false, `Too slow on challenge #${current}: ${elapsed}ms > ${timePerChallengeMs}ms`, options);
178
271
  return;
179
272
  }
180
-
181
- // Record answer and timing
273
+
274
+ // Record answer
182
275
  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
-
276
+ timings.push(elapsed);
277
+
278
+ // Validate
279
+ const valid = challenges[current].validate(msg.answer);
280
+ send(ws, { type: 'ack', id: current, valid, responseMs: elapsed });
281
+
282
+ session.current++;
283
+
197
284
  // More challenges?
198
- if (session.currentChallenge < challenges.length) {
199
- // Small delay then next challenge
200
- setTimeout(() => sendNextChallenge(session, timePerChallengeMs), 100);
285
+ if (session.current < challengeCount) {
286
+ setTimeout(() => sendNextChallenge(session, timePerChallengeMs), 50);
201
287
  } 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;
206
-
207
- finishSession(session, allPassed, allPassed ? 'Verified' : `Failed: ${passed}/${challenges.length}`, {
208
- onVerified,
209
- onFailed,
210
- passed,
211
- total: challenges.length,
212
- timings,
213
- totalTime
214
- });
288
+ // Calculate final result
289
+ const passed = answers.filter((a, i) => challenges[i].validate(a)).length;
290
+ const success = passed === challengeCount;
291
+ finishSession(session, success, success ? 'All challenges passed' : `Failed: ${passed}/${challengeCount}`, options, passed);
215
292
  }
216
293
  break;
217
-
294
+
218
295
  default:
219
- sendError(ws, 'Unknown message type: ' + msg.type);
296
+ send(ws, { type: 'error', code: 'UNKNOWN_TYPE', message: 'Unknown message type' });
220
297
  }
221
298
  }
222
299
 
223
300
  function sendNextChallenge(session, timePerChallengeMs) {
224
- const { ws, currentChallenge, challenges } = session;
225
- const challenge = challenges[currentChallenge];
226
-
227
- session.challengeStartTime = Date.now();
228
-
301
+ const { ws, current, challenges } = session;
302
+ const challenge = challenges[current];
303
+
304
+ session.challengeStart = Date.now();
305
+
229
306
  send(ws, {
230
307
  type: 'challenge',
231
- id: currentChallenge,
308
+ id: current,
232
309
  total: challenges.length,
233
- challenge: challenge.challenge_string,
310
+ challenge: challenge.challenge,
234
311
  timeLimit: timePerChallengeMs
235
312
  });
236
-
313
+
237
314
  // 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
315
+ session.timer = setTimeout(() => {
316
+ send(ws, { type: 'timeout', id: current, message: 'Time expired' });
317
+ finishSession(session, false, `Timeout on challenge #${current}`, {});
318
+ }, timePerChallengeMs + 100);
246
319
  }
247
320
 
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
-
321
+ function finishSession(session, success, message, options, passed) {
322
+ const { ws, nonce, publicId, startTime, timings } = session;
323
+ const { onVerified, onFailed, verifiedTokens } = options;
324
+
325
+ cleanup(session);
326
+
327
+ const totalTime = Date.now() - startTime;
255
328
  const result = {
256
329
  type: 'result',
257
330
  verified: success,
331
+ message,
258
332
  nonce,
259
333
  publicId,
260
- message,
261
334
  passed,
262
- total,
335
+ total: session.challenges.length,
263
336
  timings,
264
- totalTimeMs: totalTime || (Date.now() - startTime),
265
- avgResponseMs: timings?.length ? Math.round(timings.reduce((a, b) => a + b, 0) / timings.length) : null
337
+ totalTimeMs: totalTime,
338
+ avgResponseMs: timings.length ? Math.round(timings.reduce((a, b) => a + b, 0) / timings.length) : null
266
339
  };
267
-
340
+
268
341
  if (success) {
269
342
  result.role = 'AI_AGENT';
270
343
  result.sessionToken = randomBytes(32).toString('hex');
344
+
345
+ // Store verified session
346
+ if (verifiedTokens) {
347
+ verifiedTokens.set(result.sessionToken, {
348
+ publicId,
349
+ nonce,
350
+ verifiedAt: Date.now(),
351
+ expiresAt: Date.now() + 3600000, // 1 hour
352
+ totalTimeMs: totalTime,
353
+ avgResponseMs: result.avgResponseMs
354
+ });
355
+ }
356
+
271
357
  if (onVerified) onVerified(result, session);
272
358
  } else {
273
359
  if (onFailed) onFailed(result, session);
274
360
  }
275
-
361
+
276
362
  send(ws, result);
277
-
278
- // Close after sending result
279
- setTimeout(() => ws.close(), 500);
363
+ setTimeout(() => ws.close(), 300);
280
364
  }
281
365
 
282
- export default { createAAPWebSocket };
366
+ export default { createAAPWebSocket, PROTOCOL_VERSION, CHALLENGE_COUNT, TIME_PER_CHALLENGE_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 };