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.
- package/index.js +4 -4
- package/package.json +1 -1
- 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.
|
|
28
|
-
export const
|
|
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 =
|
|
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
|
-
|
|
37
|
+
TOTAL_TIME_MS,
|
|
38
38
|
CHALLENGE_COUNT
|
|
39
39
|
};
|
package/package.json
CHANGED
package/websocket.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AAP WebSocket Server v3.
|
|
2
|
+
* AAP WebSocket Server v3.1
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
9
|
+
import { randomBytes, createHash } from 'node:crypto';
|
|
10
10
|
|
|
11
11
|
// ============== CONSTANTS ==============
|
|
12
|
-
export const PROTOCOL_VERSION = '3.
|
|
12
|
+
export const PROTOCOL_VERSION = '3.1.0';
|
|
13
13
|
export const CHALLENGE_COUNT = 7;
|
|
14
|
-
export const
|
|
15
|
-
export const CONNECTION_TIMEOUT_MS =
|
|
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
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
185
|
-
message: 'Send {"type":"ready"} to
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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('
|
|
212
|
-
|
|
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,
|
|
275
|
+
export default { createAAPWebSocket, PROTOCOL_VERSION, CHALLENGE_COUNT, TOTAL_TIME_MS };
|