aap-agent-server 3.0.0 → 3.2.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.
Files changed (3) hide show
  1. package/index.js +4 -4
  2. package/package.json +1 -1
  3. package/websocket.js +168 -191
package/index.js CHANGED
@@ -24,16 +24,16 @@ export { createStore, createMemoryStore, createFileStore, createRedisStore } fro
24
24
  export * as logger from './logger.js';
25
25
 
26
26
  // Constants
27
- export const PROTOCOL_VERSION = '3.0.0';
28
- export const TIME_PER_CHALLENGE_MS = 1200;
27
+ export const PROTOCOL_VERSION = '3.2.0';
28
+ export const TOTAL_TIME_MS = 6000;
29
29
  export const CHALLENGE_COUNT = 7;
30
- export const CONNECTION_TIMEOUT_MS = 15000;
30
+ export const CONNECTION_TIMEOUT_MS = 60000;
31
31
 
32
32
  import { createAAPWebSocket } from './websocket.js';
33
33
 
34
34
  export default {
35
35
  createAAPWebSocket,
36
36
  PROTOCOL_VERSION,
37
- TIME_PER_CHALLENGE_MS,
37
+ TOTAL_TIME_MS,
38
38
  CHALLENGE_COUNT
39
39
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aap-agent-server",
3
- "version": "3.0.0",
3
+ "version": "3.2.0",
4
4
  "description": "WebSocket server for Agent Attestation Protocol - verify AI agents",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
package/websocket.js CHANGED
@@ -1,18 +1,18 @@
1
1
  /**
2
- * AAP WebSocket Server v3.0
2
+ * AAP WebSocket Server v3.2
3
3
  *
4
- * Sequential challenge delivery over persistent connection.
5
- * No preview. Server controls pacing. Humans cannot pass.
4
+ * Batch challenge + mandatory signature verification.
5
+ * No signature = no entry.
6
6
  */
7
7
 
8
8
  import { WebSocketServer } from 'ws';
9
9
  import { randomBytes, createHash, createVerify } from 'node:crypto';
10
10
 
11
11
  // ============== CONSTANTS ==============
12
- export const PROTOCOL_VERSION = '3.0.0';
12
+ export const PROTOCOL_VERSION = '3.2.0';
13
13
  export const CHALLENGE_COUNT = 7;
14
- export const TIME_PER_CHALLENGE_MS = 1200;
15
- export const CONNECTION_TIMEOUT_MS = 15000;
14
+ export const TOTAL_TIME_MS = 6000;
15
+ export const CONNECTION_TIMEOUT_MS = 60000;
16
16
 
17
17
  // ============== CHALLENGE GENERATORS ==============
18
18
  const GENERATORS = {
@@ -107,22 +107,33 @@ function generateChallenge(nonce, index) {
107
107
  const salt = createHash('sha256').update(nonce + index).digest('hex').slice(0, 6).toUpperCase();
108
108
  const seed = parseInt(nonce.slice(index * 2, index * 2 + 8), 16) || (index * 17);
109
109
  const { q, v } = GENERATORS[type](salt, seed);
110
- return { type, challenge: q, validate: v };
110
+ return { id: index, type, challenge: q, validate: v };
111
+ }
112
+
113
+ /**
114
+ * Verify secp256k1 signature
115
+ */
116
+ function verifySignature(data, signature, publicKey) {
117
+ try {
118
+ const verifier = createVerify('SHA256');
119
+ verifier.update(data);
120
+ return verifier.verify(publicKey, signature, 'base64');
121
+ } catch {
122
+ return false;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Derive public ID from public key
128
+ */
129
+ function derivePublicId(publicKey) {
130
+ return createHash('sha256').update(publicKey).digest('hex').slice(0, 16);
111
131
  }
112
132
 
113
133
  // ============== WEBSOCKET SERVER ==============
114
134
 
115
135
  /**
116
136
  * Create AAP WebSocket verification server
117
- * @param {Object} options
118
- * @param {number} [options.port] - Port for standalone server
119
- * @param {Object} [options.server] - Existing HTTP server to attach to
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
123
- * @param {Function} [options.onVerified] - Callback on successful verification
124
- * @param {Function} [options.onFailed] - Callback on failed verification
125
- * @returns {Object} { wss, sessions, close }
126
137
  */
127
138
  export function createAAPWebSocket(options = {}) {
128
139
  const {
@@ -130,46 +141,36 @@ export function createAAPWebSocket(options = {}) {
130
141
  server,
131
142
  path = '/aap',
132
143
  challengeCount = CHALLENGE_COUNT,
133
- timePerChallengeMs = TIME_PER_CHALLENGE_MS,
144
+ totalTimeMs = TOTAL_TIME_MS,
134
145
  connectionTimeoutMs = CONNECTION_TIMEOUT_MS,
146
+ requireSignature = true, // v3.2: signature required by default
135
147
  onVerified,
136
148
  onFailed
137
149
  } = options;
138
150
 
139
151
  const wssOptions = server ? { server, path } : { port };
140
152
  const wss = new WebSocketServer(wssOptions);
141
- const sessions = new Map();
142
153
  const verifiedTokens = new Map();
143
154
 
144
155
  wss.on('connection', (ws) => {
145
156
  const sessionId = randomBytes(16).toString('hex');
146
157
  const nonce = randomBytes(16).toString('hex');
147
- const startTime = Date.now();
148
-
149
- const session = {
150
- id: sessionId,
151
- ws,
152
- nonce,
153
- startTime,
154
- current: 0,
155
- challenges: [],
156
- answers: [],
157
- timings: [],
158
- publicId: null,
159
- challengeStart: null,
160
- timer: null,
161
- connTimer: null
162
- };
163
-
164
- sessions.set(sessionId, session);
158
+ let challenges = [];
159
+ let validators = [];
160
+ let challengesSentAt = null;
161
+ let publicKey = null;
162
+ let publicId = null;
163
+ let answered = false;
165
164
 
166
165
  // Generate challenges
167
166
  for (let i = 0; i < challengeCount; i++) {
168
- session.challenges.push(generateChallenge(nonce, i));
167
+ const ch = generateChallenge(nonce, i);
168
+ challenges.push({ id: ch.id, type: ch.type, challenge: ch.challenge });
169
+ validators.push(ch.validate);
169
170
  }
170
171
 
171
172
  // Connection timeout
172
- session.connTimer = setTimeout(() => {
173
+ const connTimer = setTimeout(() => {
173
174
  send(ws, { type: 'error', code: 'TIMEOUT', message: 'Connection timeout' });
174
175
  ws.close();
175
176
  }, connectionTimeoutMs);
@@ -180,9 +181,11 @@ export function createAAPWebSocket(options = {}) {
180
181
  sessionId,
181
182
  protocol: 'AAP',
182
183
  version: PROTOCOL_VERSION,
184
+ mode: 'batch',
183
185
  challengeCount,
184
- timePerChallengeMs,
185
- message: 'Send {"type":"ready"} to begin verification.'
186
+ totalTimeMs,
187
+ requireSignature,
188
+ message: 'Send {"type":"ready","publicKey":"..."} to receive challenges.'
186
189
  });
187
190
 
188
191
  ws.on('message', (data) => {
@@ -194,173 +197,147 @@ export function createAAPWebSocket(options = {}) {
194
197
  return;
195
198
  }
196
199
 
197
- handleMessage(session, msg, {
198
- challengeCount,
199
- timePerChallengeMs,
200
- onVerified,
201
- onFailed,
202
- verifiedTokens
203
- });
204
- });
205
-
206
- ws.on('close', () => {
207
- cleanup(session);
208
- sessions.delete(sessionId);
200
+ if (msg.type === 'ready' && !challengesSentAt) {
201
+ // v3.2: publicKey required
202
+ if (requireSignature && !msg.publicKey) {
203
+ send(ws, { type: 'error', code: 'MISSING_PUBLIC_KEY', message: 'publicKey required for signature verification' });
204
+ return;
205
+ }
206
+
207
+ publicKey = msg.publicKey || null;
208
+ publicId = publicKey ? derivePublicId(publicKey) : 'anon-' + randomBytes(4).toString('hex');
209
+ challengesSentAt = Date.now();
210
+
211
+ // Send all challenges at once
212
+ send(ws, {
213
+ type: 'challenges',
214
+ nonce,
215
+ challenges,
216
+ totalTimeMs,
217
+ expiresAt: challengesSentAt + totalTimeMs
218
+ });
219
+ }
220
+ else if (msg.type === 'answers' && challengesSentAt && !answered) {
221
+ answered = true;
222
+ clearTimeout(connTimer);
223
+
224
+ const elapsed = Date.now() - challengesSentAt;
225
+ const answers = msg.answers || [];
226
+ const signature = msg.signature;
227
+ const timestamp = msg.timestamp;
228
+
229
+ // v3.2: Verify signature first
230
+ if (requireSignature) {
231
+ if (!signature) {
232
+ const result = {
233
+ type: 'result',
234
+ verified: false,
235
+ message: 'Missing signature',
236
+ code: 'MISSING_SIGNATURE',
237
+ publicId
238
+ };
239
+ if (onFailed) onFailed(result);
240
+ send(ws, result);
241
+ setTimeout(() => ws.close(), 300);
242
+ return;
243
+ }
244
+
245
+ // Create proof data for verification
246
+ const proofData = JSON.stringify({ nonce, answers, publicId, timestamp });
247
+
248
+ if (!verifySignature(proofData, signature, publicKey)) {
249
+ const result = {
250
+ type: 'result',
251
+ verified: false,
252
+ message: 'Invalid signature',
253
+ code: 'INVALID_SIGNATURE',
254
+ publicId
255
+ };
256
+ if (onFailed) onFailed(result);
257
+ send(ws, result);
258
+ setTimeout(() => ws.close(), 300);
259
+ return;
260
+ }
261
+ }
262
+
263
+ // Too slow?
264
+ if (elapsed > totalTimeMs) {
265
+ const result = {
266
+ type: 'result',
267
+ verified: false,
268
+ message: `Too slow: ${elapsed}ms > ${totalTimeMs}ms`,
269
+ code: 'TOO_SLOW',
270
+ publicId,
271
+ responseTimeMs: elapsed
272
+ };
273
+ if (onFailed) onFailed(result);
274
+ send(ws, result);
275
+ setTimeout(() => ws.close(), 300);
276
+ return;
277
+ }
278
+
279
+ // Validate all answers
280
+ let passed = 0;
281
+ const results = [];
282
+ for (let i = 0; i < challengeCount; i++) {
283
+ const valid = validators[i](answers[i]);
284
+ if (valid) passed++;
285
+ results.push({ id: i, valid });
286
+ }
287
+
288
+ const success = passed === challengeCount;
289
+ const result = {
290
+ type: 'result',
291
+ verified: success,
292
+ message: success ? 'All challenges passed' : `Failed: ${passed}/${challengeCount}`,
293
+ publicId,
294
+ passed,
295
+ total: challengeCount,
296
+ results,
297
+ responseTimeMs: elapsed
298
+ };
299
+
300
+ if (success) {
301
+ result.role = 'AI_AGENT';
302
+ result.sessionToken = randomBytes(32).toString('hex');
303
+
304
+ verifiedTokens.set(result.sessionToken, {
305
+ publicId,
306
+ publicKey,
307
+ nonce,
308
+ verifiedAt: Date.now(),
309
+ expiresAt: Date.now() + 3600000,
310
+ responseTimeMs: elapsed
311
+ });
312
+
313
+ if (onVerified) onVerified(result);
314
+ } else {
315
+ if (onFailed) onFailed(result);
316
+ }
317
+
318
+ send(ws, result);
319
+ setTimeout(() => ws.close(), 300);
320
+ }
209
321
  });
210
322
 
211
- ws.on('error', () => {
212
- cleanup(session);
213
- sessions.delete(sessionId);
214
- });
323
+ ws.on('close', () => clearTimeout(connTimer));
324
+ ws.on('error', () => clearTimeout(connTimer));
215
325
  });
216
326
 
217
327
  return {
218
328
  wss,
219
- sessions,
220
329
  verifiedTokens,
221
330
  close: () => wss.close(),
222
-
223
- // Helper to check if token is verified
224
331
  isVerified: (token) => {
225
332
  const session = verifiedTokens.get(token);
226
333
  return session && Date.now() < session.expiresAt;
227
334
  },
228
-
229
- // Get verified session info
230
335
  getSession: (token) => verifiedTokens.get(token)
231
336
  };
232
337
  }
233
338
 
234
339
  function send(ws, data) {
235
- if (ws.readyState === 1) {
236
- ws.send(JSON.stringify(data));
237
- }
238
- }
239
-
240
- function cleanup(session) {
241
- clearTimeout(session.timer);
242
- clearTimeout(session.connTimer);
243
- }
244
-
245
- function handleMessage(session, msg, options) {
246
- const { ws, current, challenges, answers, timings } = session;
247
- const { challengeCount, timePerChallengeMs, onVerified, onFailed, verifiedTokens } = options;
248
-
249
- switch (msg.type) {
250
- case 'ready':
251
- if (current !== 0) {
252
- send(ws, { type: 'error', code: 'ALREADY_STARTED', message: 'Already started' });
253
- return;
254
- }
255
- session.publicId = msg.publicId || 'anon-' + randomBytes(4).toString('hex');
256
- sendNextChallenge(session, timePerChallengeMs);
257
- break;
258
-
259
- case 'answer':
260
- if (session.challengeStart === null) {
261
- send(ws, { type: 'error', code: 'NO_CHALLENGE', message: 'No active challenge' });
262
- return;
263
- }
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);
271
- return;
272
- }
273
-
274
- // Record answer
275
- answers.push(msg.answer);
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
-
284
- // More challenges?
285
- if (session.current < challengeCount) {
286
- setTimeout(() => sendNextChallenge(session, timePerChallengeMs), 50);
287
- } else {
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);
292
- }
293
- break;
294
-
295
- default:
296
- send(ws, { type: 'error', code: 'UNKNOWN_TYPE', message: 'Unknown message type' });
297
- }
298
- }
299
-
300
- function sendNextChallenge(session, timePerChallengeMs) {
301
- const { ws, current, challenges } = session;
302
- const challenge = challenges[current];
303
-
304
- session.challengeStart = Date.now();
305
-
306
- send(ws, {
307
- type: 'challenge',
308
- id: current,
309
- total: challenges.length,
310
- challenge: challenge.challenge,
311
- timeLimit: timePerChallengeMs
312
- });
313
-
314
- // Timeout for this challenge
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);
319
- }
320
-
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;
328
- const result = {
329
- type: 'result',
330
- verified: success,
331
- message,
332
- nonce,
333
- publicId,
334
- passed,
335
- total: session.challenges.length,
336
- timings,
337
- totalTimeMs: totalTime,
338
- avgResponseMs: timings.length ? Math.round(timings.reduce((a, b) => a + b, 0) / timings.length) : null
339
- };
340
-
341
- if (success) {
342
- result.role = 'AI_AGENT';
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
-
357
- if (onVerified) onVerified(result, session);
358
- } else {
359
- if (onFailed) onFailed(result, session);
360
- }
361
-
362
- send(ws, result);
363
- setTimeout(() => ws.close(), 300);
340
+ if (ws.readyState === 1) ws.send(JSON.stringify(data));
364
341
  }
365
342
 
366
- export default { createAAPWebSocket, PROTOCOL_VERSION, CHALLENGE_COUNT, TIME_PER_CHALLENGE_MS };
343
+ export default { createAAPWebSocket, PROTOCOL_VERSION, CHALLENGE_COUNT, TOTAL_TIME_MS };