aap-agent-server 2.6.0 → 2.7.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 +6 -1
- package/package.json +3 -2
- package/websocket.js +282 -0
package/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @aap/server v2.
|
|
2
|
+
* @aap/server v2.7.0
|
|
3
3
|
*
|
|
4
4
|
* Server-side utilities for Agent Attestation Protocol.
|
|
5
5
|
* The Reverse Turing Test - CAPTCHAs block bots, AAP blocks humans.
|
|
@@ -11,6 +11,7 @@ export * from './ratelimit.js';
|
|
|
11
11
|
export * from './whitelist.js';
|
|
12
12
|
export * from './persistence.js';
|
|
13
13
|
export * from './errors.js';
|
|
14
|
+
export * from './websocket.js';
|
|
14
15
|
export * as logger from './logger.js';
|
|
15
16
|
|
|
16
17
|
import { aapMiddleware, createRouter } from './middleware.js';
|
|
@@ -18,6 +19,7 @@ import challenges from './challenges.js';
|
|
|
18
19
|
import { createRateLimiter, createFailureLimiter } from './ratelimit.js';
|
|
19
20
|
import { createWhitelist, createKeyRotation } from './whitelist.js';
|
|
20
21
|
import { createStore, createMemoryStore, createFileStore, createRedisStore } from './persistence.js';
|
|
22
|
+
import { createAAPWebSocket } from './websocket.js';
|
|
21
23
|
|
|
22
24
|
export { challenges };
|
|
23
25
|
|
|
@@ -27,6 +29,9 @@ export default {
|
|
|
27
29
|
createRouter,
|
|
28
30
|
challenges,
|
|
29
31
|
|
|
32
|
+
// WebSocket (v2.7+)
|
|
33
|
+
createAAPWebSocket,
|
|
34
|
+
|
|
30
35
|
// Security
|
|
31
36
|
createRateLimiter,
|
|
32
37
|
createFailureLimiter,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aap-agent-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"description": "Server middleware for Agent Attestation Protocol - verify AI agents",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
},
|
|
22
22
|
"homepage": "https://github.com/ira-hash/agent-attestation-protocol#readme",
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"aap-agent-core": "^2.6.0"
|
|
24
|
+
"aap-agent-core": "^2.6.0",
|
|
25
|
+
"ws": "^8.16.0"
|
|
25
26
|
},
|
|
26
27
|
"peerDependencies": {
|
|
27
28
|
"express": "^4.18.0 || ^5.0.0"
|
package/websocket.js
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AAP WebSocket Server v2.7
|
|
3
|
+
*
|
|
4
|
+
* Sequential challenge delivery over persistent connection.
|
|
5
|
+
* Humans cannot preview questions - each arrives with strict time limit.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { WebSocketServer } from 'ws';
|
|
9
|
+
import { randomBytes, createVerify } from 'node:crypto';
|
|
10
|
+
import { generate as generateChallenge, getTypes } from './challenges.js';
|
|
11
|
+
|
|
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';
|
|
17
|
+
|
|
18
|
+
/**
|
|
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
|
|
26
|
+
*/
|
|
27
|
+
export function createAAPWebSocket(options = {}) {
|
|
28
|
+
const {
|
|
29
|
+
port,
|
|
30
|
+
httpServer,
|
|
31
|
+
path = '/aap',
|
|
32
|
+
onVerified = null,
|
|
33
|
+
onFailed = null,
|
|
34
|
+
challengeCount = CHALLENGE_COUNT,
|
|
35
|
+
timePerChallengeMs = TIME_PER_CHALLENGE_MS
|
|
36
|
+
} = options;
|
|
37
|
+
|
|
38
|
+
const wssOptions = httpServer
|
|
39
|
+
? { server: httpServer, path }
|
|
40
|
+
: { port, path };
|
|
41
|
+
|
|
42
|
+
const wss = new WebSocketServer(wssOptions);
|
|
43
|
+
|
|
44
|
+
// Track active sessions
|
|
45
|
+
const sessions = new Map();
|
|
46
|
+
|
|
47
|
+
wss.on('connection', (ws, req) => {
|
|
48
|
+
const sessionId = randomBytes(16).toString('hex');
|
|
49
|
+
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();
|
|
79
|
+
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);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Send handshake
|
|
91
|
+
send(ws, {
|
|
92
|
+
type: 'handshake',
|
|
93
|
+
sessionId,
|
|
94
|
+
nonce,
|
|
95
|
+
protocol: 'AAP',
|
|
96
|
+
version: PROTOCOL_VERSION,
|
|
97
|
+
mode: 'websocket',
|
|
98
|
+
challengeCount,
|
|
99
|
+
timePerChallengeMs,
|
|
100
|
+
message: 'Connected. Send "ready" with publicKey to begin.'
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
ws.on('message', (data) => {
|
|
104
|
+
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');
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
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 });
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
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;
|
|
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
|
+
});
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
|
|
218
|
+
default:
|
|
219
|
+
sendError(ws, 'Unknown message type: ' + msg.type);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
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
|
|
235
|
+
});
|
|
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
|
+
|
|
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
|
|
266
|
+
};
|
|
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
|
+
}
|
|
281
|
+
|
|
282
|
+
export default { createAAPWebSocket };
|