aap-agent-server 3.0.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.
Files changed (3) hide show
  1. package/index.js +4 -4
  2. package/package.json +1 -1
  3. package/websocket.js +101 -192
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.1.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.1.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.1
3
3
  *
4
- * Sequential challenge delivery over persistent connection.
5
- * No preview. Server controls pacing. Humans cannot pass.
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, createHash, createVerify } from 'node:crypto';
9
+ import { randomBytes, createHash } from 'node:crypto';
10
10
 
11
11
  // ============== CONSTANTS ==============
12
- export const PROTOCOL_VERSION = '3.0.0';
12
+ export const PROTOCOL_VERSION = '3.1.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,13 @@ 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
111
  }
112
112
 
113
113
  // ============== WEBSOCKET SERVER ==============
114
114
 
115
115
  /**
116
116
  * 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
117
  */
127
118
  export function createAAPWebSocket(options = {}) {
128
119
  const {
@@ -130,7 +121,7 @@ export function createAAPWebSocket(options = {}) {
130
121
  server,
131
122
  path = '/aap',
132
123
  challengeCount = CHALLENGE_COUNT,
133
- timePerChallengeMs = TIME_PER_CHALLENGE_MS,
124
+ totalTimeMs = TOTAL_TIME_MS,
134
125
  connectionTimeoutMs = CONNECTION_TIMEOUT_MS,
135
126
  onVerified,
136
127
  onFailed
@@ -138,38 +129,26 @@ export function createAAPWebSocket(options = {}) {
138
129
 
139
130
  const wssOptions = server ? { server, path } : { port };
140
131
  const wss = new WebSocketServer(wssOptions);
141
- const sessions = new Map();
142
132
  const verifiedTokens = new Map();
143
133
 
144
134
  wss.on('connection', (ws) => {
145
135
  const sessionId = randomBytes(16).toString('hex');
146
136
  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);
137
+ let challenges = [];
138
+ let validators = [];
139
+ let challengesSentAt = null;
140
+ let publicId = null;
141
+ let answered = false;
165
142
 
166
143
  // Generate challenges
167
144
  for (let i = 0; i < challengeCount; i++) {
168
- session.challenges.push(generateChallenge(nonce, i));
145
+ const ch = generateChallenge(nonce, i);
146
+ challenges.push({ id: ch.id, type: ch.type, challenge: ch.challenge });
147
+ validators.push(ch.validate);
169
148
  }
170
149
 
171
150
  // Connection timeout
172
- session.connTimer = setTimeout(() => {
151
+ const connTimer = setTimeout(() => {
173
152
  send(ws, { type: 'error', code: 'TIMEOUT', message: 'Connection timeout' });
174
153
  ws.close();
175
154
  }, connectionTimeoutMs);
@@ -180,9 +159,10 @@ export function createAAPWebSocket(options = {}) {
180
159
  sessionId,
181
160
  protocol: 'AAP',
182
161
  version: PROTOCOL_VERSION,
162
+ mode: 'batch',
183
163
  challengeCount,
184
- timePerChallengeMs,
185
- message: 'Send {"type":"ready"} to begin verification.'
164
+ totalTimeMs,
165
+ message: 'Send {"type":"ready"} to receive challenges.'
186
166
  });
187
167
 
188
168
  ws.on('message', (data) => {
@@ -194,173 +174,102 @@ export function createAAPWebSocket(options = {}) {
194
174
  return;
195
175
  }
196
176
 
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);
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
182
+ send(ws, {
183
+ type: 'challenges',
184
+ nonce,
185
+ challenges,
186
+ totalTimeMs,
187
+ expiresAt: challengesSentAt + totalTimeMs
188
+ });
189
+ }
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 || [];
196
+
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,
227
+ passed,
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);
252
+ }
209
253
  });
210
254
 
211
- ws.on('error', () => {
212
- cleanup(session);
213
- sessions.delete(sessionId);
214
- });
255
+ ws.on('close', () => clearTimeout(connTimer));
256
+ ws.on('error', () => clearTimeout(connTimer));
215
257
  });
216
258
 
217
259
  return {
218
260
  wss,
219
- sessions,
220
261
  verifiedTokens,
221
262
  close: () => wss.close(),
222
-
223
- // Helper to check if token is verified
224
263
  isVerified: (token) => {
225
264
  const session = verifiedTokens.get(token);
226
265
  return session && Date.now() < session.expiresAt;
227
266
  },
228
-
229
- // Get verified session info
230
267
  getSession: (token) => verifiedTokens.get(token)
231
268
  };
232
269
  }
233
270
 
234
271
  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);
272
+ if (ws.readyState === 1) ws.send(JSON.stringify(data));
364
273
  }
365
274
 
366
- export default { createAAPWebSocket, PROTOCOL_VERSION, CHALLENGE_COUNT, TIME_PER_CHALLENGE_MS };
275
+ export default { createAAPWebSocket, PROTOCOL_VERSION, CHALLENGE_COUNT, TOTAL_TIME_MS };