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.
- package/index.js +4 -4
- package/package.json +1 -1
- 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.
|
|
28
|
-
export const
|
|
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 =
|
|
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.2
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* No
|
|
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.
|
|
12
|
+
export const PROTOCOL_VERSION = '3.2.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,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
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
185
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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('
|
|
212
|
-
|
|
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,
|
|
343
|
+
export default { createAAPWebSocket, PROTOCOL_VERSION, CHALLENGE_COUNT, TOTAL_TIME_MS };
|