aap-agent-server 2.6.0 → 3.0.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 +29 -31
- package/package.json +5 -14
- package/websocket.js +366 -0
- package/errors.js +0 -134
- package/middleware.js +0 -420
- package/ratelimit.js +0 -117
package/index.js
CHANGED
|
@@ -1,41 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @aap/server
|
|
2
|
+
* @aap/server v3.0.0
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* WebSocket-only Agent Attestation Protocol.
|
|
5
|
+
* Sequential challenges - no preview, no mercy.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export
|
|
13
|
-
|
|
8
|
+
// Core WebSocket server
|
|
9
|
+
export { createAAPWebSocket } from './websocket.js';
|
|
10
|
+
|
|
11
|
+
// Challenge generation (internal use)
|
|
12
|
+
export {
|
|
13
|
+
generate,
|
|
14
|
+
generateBatch,
|
|
15
|
+
validateBatch,
|
|
16
|
+
getTypes,
|
|
17
|
+
BATCH_SIZE,
|
|
18
|
+
CHALLENGE_TYPES
|
|
19
|
+
} from './challenges.js';
|
|
20
|
+
|
|
21
|
+
// Optional utilities
|
|
22
|
+
export { createWhitelist, createKeyRotation } from './whitelist.js';
|
|
23
|
+
export { createStore, createMemoryStore, createFileStore, createRedisStore } from './persistence.js';
|
|
14
24
|
export * as logger from './logger.js';
|
|
15
25
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
// Constants
|
|
27
|
+
export const PROTOCOL_VERSION = '3.0.0';
|
|
28
|
+
export const TIME_PER_CHALLENGE_MS = 1200;
|
|
29
|
+
export const CHALLENGE_COUNT = 7;
|
|
30
|
+
export const CONNECTION_TIMEOUT_MS = 15000;
|
|
21
31
|
|
|
22
|
-
|
|
32
|
+
import { createAAPWebSocket } from './websocket.js';
|
|
23
33
|
|
|
24
34
|
export default {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// Security
|
|
31
|
-
createRateLimiter,
|
|
32
|
-
createFailureLimiter,
|
|
33
|
-
createWhitelist,
|
|
34
|
-
createKeyRotation,
|
|
35
|
-
|
|
36
|
-
// Persistence
|
|
37
|
-
createStore,
|
|
38
|
-
createMemoryStore,
|
|
39
|
-
createFileStore,
|
|
40
|
-
createRedisStore
|
|
35
|
+
createAAPWebSocket,
|
|
36
|
+
PROTOCOL_VERSION,
|
|
37
|
+
TIME_PER_CHALLENGE_MS,
|
|
38
|
+
CHALLENGE_COUNT
|
|
41
39
|
};
|
package/package.json
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aap-agent-server",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "WebSocket server for Agent Attestation Protocol - verify AI agents",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": "./index.js",
|
|
10
|
-
"./
|
|
11
|
-
"./challenges": "./challenges.js"
|
|
10
|
+
"./websocket": "./websocket.js"
|
|
12
11
|
},
|
|
13
12
|
"files": ["*.js", "README.md"],
|
|
14
|
-
"keywords": ["aap", "agent", "attestation", "verification", "
|
|
13
|
+
"keywords": ["aap", "agent", "attestation", "verification", "websocket", "ai"],
|
|
15
14
|
"author": "ira-hash",
|
|
16
15
|
"license": "MIT",
|
|
17
16
|
"repository": {
|
|
@@ -21,15 +20,7 @@
|
|
|
21
20
|
},
|
|
22
21
|
"homepage": "https://github.com/ira-hash/agent-attestation-protocol#readme",
|
|
23
22
|
"dependencies": {
|
|
24
|
-
"
|
|
25
|
-
},
|
|
26
|
-
"peerDependencies": {
|
|
27
|
-
"express": "^4.18.0 || ^5.0.0"
|
|
28
|
-
},
|
|
29
|
-
"peerDependenciesMeta": {
|
|
30
|
-
"express": {
|
|
31
|
-
"optional": true
|
|
32
|
-
}
|
|
23
|
+
"ws": "^8.16.0"
|
|
33
24
|
},
|
|
34
25
|
"engines": {
|
|
35
26
|
"node": ">=18.0.0"
|
package/websocket.js
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AAP WebSocket Server v3.0
|
|
3
|
+
*
|
|
4
|
+
* Sequential challenge delivery over persistent connection.
|
|
5
|
+
* No preview. Server controls pacing. Humans cannot pass.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { WebSocketServer } from 'ws';
|
|
9
|
+
import { randomBytes, createHash, createVerify } from 'node:crypto';
|
|
10
|
+
|
|
11
|
+
// ============== CONSTANTS ==============
|
|
12
|
+
export const PROTOCOL_VERSION = '3.0.0';
|
|
13
|
+
export const CHALLENGE_COUNT = 7;
|
|
14
|
+
export const TIME_PER_CHALLENGE_MS = 1200;
|
|
15
|
+
export const CONNECTION_TIMEOUT_MS = 15000;
|
|
16
|
+
|
|
17
|
+
// ============== CHALLENGE GENERATORS ==============
|
|
18
|
+
const GENERATORS = {
|
|
19
|
+
math: (salt, seed) => {
|
|
20
|
+
const a = 10 + (seed % 90);
|
|
21
|
+
const b = 10 + ((seed * 7) % 90);
|
|
22
|
+
const ops = ['+', '-', '*'];
|
|
23
|
+
const op = ops[seed % 3];
|
|
24
|
+
const answer = op === '+' ? a + b : op === '-' ? a - b : a * b;
|
|
25
|
+
return {
|
|
26
|
+
q: `[REQ-${salt}] What is ${a} ${op} ${b}?\nFormat: {"salt":"${salt}","result":number}`,
|
|
27
|
+
v: (r) => r?.salt === salt && r?.result === answer
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
logic: (salt, seed) => {
|
|
32
|
+
const x = 10 + (seed % 50);
|
|
33
|
+
const y = 10 + ((seed * 3) % 50);
|
|
34
|
+
const answer = x > y ? 'GREATER' : x < y ? 'LESS' : 'EQUAL';
|
|
35
|
+
return {
|
|
36
|
+
q: `[REQ-${salt}] X=${x}, Y=${y}. Answer "GREATER" if X>Y, "LESS" if X<Y, "EQUAL" if X=Y.\nFormat: {"salt":"${salt}","answer":"..."}`,
|
|
37
|
+
v: (r) => r?.salt === salt && r?.answer === answer
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
count: (salt, seed) => {
|
|
42
|
+
const all = ['cat','dog','apple','bird','car','fish','tree','book','lion','grape'];
|
|
43
|
+
const animals = ['cat','dog','bird','fish','lion'];
|
|
44
|
+
const n = 4 + (seed % 5);
|
|
45
|
+
const items = [];
|
|
46
|
+
for (let i = 0; i < n; i++) {
|
|
47
|
+
items.push(all[(seed + i * 3) % all.length]);
|
|
48
|
+
}
|
|
49
|
+
const count = items.filter(i => animals.includes(i)).length;
|
|
50
|
+
return {
|
|
51
|
+
q: `[REQ-${salt}] Count animals: ${items.join(', ')}\nFormat: {"salt":"${salt}","count":number}`,
|
|
52
|
+
v: (r) => r?.salt === salt && r?.count === count
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
pattern: (salt, seed) => {
|
|
57
|
+
const start = 2 + (seed % 10);
|
|
58
|
+
const step = 2 + (seed % 6);
|
|
59
|
+
const seq = [start, start+step, start+step*2, start+step*3];
|
|
60
|
+
const next = [start+step*4, start+step*5];
|
|
61
|
+
return {
|
|
62
|
+
q: `[REQ-${salt}] Next 2 numbers: [${seq.join(', ')}, ?, ?]\nFormat: {"salt":"${salt}","next":[n1,n2]}`,
|
|
63
|
+
v: (r) => r?.salt === salt && Array.isArray(r?.next) && r.next[0] === next[0] && r.next[1] === next[1]
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
reverse: (salt, seed) => {
|
|
68
|
+
const words = ['hello','world','agent','robot','claw','alpha','delta'];
|
|
69
|
+
const word = words[seed % words.length];
|
|
70
|
+
const rev = word.split('').reverse().join('');
|
|
71
|
+
return {
|
|
72
|
+
q: `[REQ-${salt}] Reverse the string: "${word}"\nFormat: {"salt":"${salt}","result":"..."}`,
|
|
73
|
+
v: (r) => r?.salt === salt && r?.result === rev
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
extract: (salt, seed) => {
|
|
78
|
+
const colors = ['red','blue','green','yellow','purple','orange'];
|
|
79
|
+
const color = colors[seed % colors.length];
|
|
80
|
+
const nouns = ['car','robot','bird','house'];
|
|
81
|
+
const noun = nouns[seed % nouns.length];
|
|
82
|
+
return {
|
|
83
|
+
q: `[REQ-${salt}] Extract the color: "The ${color} ${noun} moved quickly"\nFormat: {"salt":"${salt}","color":"..."}`,
|
|
84
|
+
v: (r) => r?.salt === salt && r?.color === color
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
longest: (salt, seed) => {
|
|
89
|
+
const sets = [
|
|
90
|
+
['cat', 'elephant', 'dog', 'ant'],
|
|
91
|
+
['bee', 'hippopotamus', 'fox', 'rat'],
|
|
92
|
+
['owl', 'crocodile', 'bat', 'fly']
|
|
93
|
+
];
|
|
94
|
+
const words = sets[seed % sets.length];
|
|
95
|
+
const longest = words.reduce((a, b) => a.length >= b.length ? a : b);
|
|
96
|
+
return {
|
|
97
|
+
q: `[REQ-${salt}] Find longest word: ${words.join(', ')}\nFormat: {"salt":"${salt}","answer":"..."}`,
|
|
98
|
+
v: (r) => r?.salt === salt && r?.answer === longest
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const TYPES = Object.keys(GENERATORS);
|
|
104
|
+
|
|
105
|
+
function generateChallenge(nonce, index) {
|
|
106
|
+
const type = TYPES[index % TYPES.length];
|
|
107
|
+
const salt = createHash('sha256').update(nonce + index).digest('hex').slice(0, 6).toUpperCase();
|
|
108
|
+
const seed = parseInt(nonce.slice(index * 2, index * 2 + 8), 16) || (index * 17);
|
|
109
|
+
const { q, v } = GENERATORS[type](salt, seed);
|
|
110
|
+
return { type, challenge: q, validate: v };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ============== WEBSOCKET SERVER ==============
|
|
114
|
+
|
|
115
|
+
/**
|
|
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
|
+
*/
|
|
127
|
+
export function createAAPWebSocket(options = {}) {
|
|
128
|
+
const {
|
|
129
|
+
port,
|
|
130
|
+
server,
|
|
131
|
+
path = '/aap',
|
|
132
|
+
challengeCount = CHALLENGE_COUNT,
|
|
133
|
+
timePerChallengeMs = TIME_PER_CHALLENGE_MS,
|
|
134
|
+
connectionTimeoutMs = CONNECTION_TIMEOUT_MS,
|
|
135
|
+
onVerified,
|
|
136
|
+
onFailed
|
|
137
|
+
} = options;
|
|
138
|
+
|
|
139
|
+
const wssOptions = server ? { server, path } : { port };
|
|
140
|
+
const wss = new WebSocketServer(wssOptions);
|
|
141
|
+
const sessions = new Map();
|
|
142
|
+
const verifiedTokens = new Map();
|
|
143
|
+
|
|
144
|
+
wss.on('connection', (ws) => {
|
|
145
|
+
const sessionId = randomBytes(16).toString('hex');
|
|
146
|
+
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);
|
|
165
|
+
|
|
166
|
+
// Generate challenges
|
|
167
|
+
for (let i = 0; i < challengeCount; i++) {
|
|
168
|
+
session.challenges.push(generateChallenge(nonce, i));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Connection timeout
|
|
172
|
+
session.connTimer = setTimeout(() => {
|
|
173
|
+
send(ws, { type: 'error', code: 'TIMEOUT', message: 'Connection timeout' });
|
|
174
|
+
ws.close();
|
|
175
|
+
}, connectionTimeoutMs);
|
|
176
|
+
|
|
177
|
+
// Send handshake
|
|
178
|
+
send(ws, {
|
|
179
|
+
type: 'handshake',
|
|
180
|
+
sessionId,
|
|
181
|
+
protocol: 'AAP',
|
|
182
|
+
version: PROTOCOL_VERSION,
|
|
183
|
+
challengeCount,
|
|
184
|
+
timePerChallengeMs,
|
|
185
|
+
message: 'Send {"type":"ready"} to begin verification.'
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
ws.on('message', (data) => {
|
|
189
|
+
let msg;
|
|
190
|
+
try {
|
|
191
|
+
msg = JSON.parse(data.toString());
|
|
192
|
+
} catch {
|
|
193
|
+
send(ws, { type: 'error', code: 'INVALID_JSON', message: 'Invalid JSON' });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
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);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
ws.on('error', () => {
|
|
212
|
+
cleanup(session);
|
|
213
|
+
sessions.delete(sessionId);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
wss,
|
|
219
|
+
sessions,
|
|
220
|
+
verifiedTokens,
|
|
221
|
+
close: () => wss.close(),
|
|
222
|
+
|
|
223
|
+
// Helper to check if token is verified
|
|
224
|
+
isVerified: (token) => {
|
|
225
|
+
const session = verifiedTokens.get(token);
|
|
226
|
+
return session && Date.now() < session.expiresAt;
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
// Get verified session info
|
|
230
|
+
getSession: (token) => verifiedTokens.get(token)
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
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);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export default { createAAPWebSocket, PROTOCOL_VERSION, CHALLENGE_COUNT, TIME_PER_CHALLENGE_MS };
|
package/errors.js
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AAP Server - Error Definitions
|
|
3
|
-
*
|
|
4
|
-
* Consistent, client-friendly error messages
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export const ErrorCodes = {
|
|
8
|
-
// Challenge errors
|
|
9
|
-
CHALLENGE_NOT_FOUND: 'CHALLENGE_NOT_FOUND',
|
|
10
|
-
CHALLENGE_EXPIRED: 'CHALLENGE_EXPIRED',
|
|
11
|
-
CHALLENGE_ALREADY_USED: 'CHALLENGE_ALREADY_USED',
|
|
12
|
-
|
|
13
|
-
// Solution errors
|
|
14
|
-
MISSING_SOLUTIONS: 'MISSING_SOLUTIONS',
|
|
15
|
-
INVALID_SOLUTIONS_COUNT: 'INVALID_SOLUTIONS_COUNT',
|
|
16
|
-
SOLUTION_VALIDATION_FAILED: 'SOLUTION_VALIDATION_FAILED',
|
|
17
|
-
|
|
18
|
-
// Timing errors
|
|
19
|
-
RESPONSE_TOO_SLOW: 'RESPONSE_TOO_SLOW',
|
|
20
|
-
|
|
21
|
-
// Signature errors
|
|
22
|
-
INVALID_SIGNATURE: 'INVALID_SIGNATURE',
|
|
23
|
-
MISSING_SIGNATURE: 'MISSING_SIGNATURE',
|
|
24
|
-
INVALID_PUBLIC_KEY: 'INVALID_PUBLIC_KEY',
|
|
25
|
-
|
|
26
|
-
// General errors
|
|
27
|
-
INVALID_REQUEST: 'INVALID_REQUEST',
|
|
28
|
-
INTERNAL_ERROR: 'INTERNAL_ERROR',
|
|
29
|
-
RATE_LIMITED: 'RATE_LIMITED'
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export const ErrorMessages = {
|
|
33
|
-
[ErrorCodes.CHALLENGE_NOT_FOUND]: {
|
|
34
|
-
message: 'Challenge not found',
|
|
35
|
-
hint: 'Request a new challenge via POST /challenge',
|
|
36
|
-
status: 400
|
|
37
|
-
},
|
|
38
|
-
[ErrorCodes.CHALLENGE_EXPIRED]: {
|
|
39
|
-
message: 'Challenge has expired',
|
|
40
|
-
hint: 'Challenges expire after 60 seconds. Request a new one.',
|
|
41
|
-
status: 400
|
|
42
|
-
},
|
|
43
|
-
[ErrorCodes.CHALLENGE_ALREADY_USED]: {
|
|
44
|
-
message: 'Challenge already used',
|
|
45
|
-
hint: 'Each challenge can only be used once. Request a new one.',
|
|
46
|
-
status: 400
|
|
47
|
-
},
|
|
48
|
-
[ErrorCodes.MISSING_SOLUTIONS]: {
|
|
49
|
-
message: 'Missing solutions array',
|
|
50
|
-
hint: 'Include "solutions" array in request body',
|
|
51
|
-
status: 400
|
|
52
|
-
},
|
|
53
|
-
[ErrorCodes.INVALID_SOLUTIONS_COUNT]: {
|
|
54
|
-
message: 'Invalid number of solutions',
|
|
55
|
-
hint: 'Provide exactly 3 solutions for batch challenges',
|
|
56
|
-
status: 400
|
|
57
|
-
},
|
|
58
|
-
[ErrorCodes.SOLUTION_VALIDATION_FAILED]: {
|
|
59
|
-
message: 'Solution validation failed (Proof of Intelligence)',
|
|
60
|
-
hint: 'One or more solutions are incorrect. Ensure your LLM correctly solves each challenge.',
|
|
61
|
-
status: 400
|
|
62
|
-
},
|
|
63
|
-
[ErrorCodes.RESPONSE_TOO_SLOW]: {
|
|
64
|
-
message: 'Response too slow (Proof of Liveness failed)',
|
|
65
|
-
hint: 'Response must arrive within 12 seconds for batch challenges',
|
|
66
|
-
status: 400
|
|
67
|
-
},
|
|
68
|
-
[ErrorCodes.INVALID_SIGNATURE]: {
|
|
69
|
-
message: 'Invalid signature (Proof of Identity failed)',
|
|
70
|
-
hint: 'Ensure you sign the correct data with your private key',
|
|
71
|
-
status: 400
|
|
72
|
-
},
|
|
73
|
-
[ErrorCodes.MISSING_SIGNATURE]: {
|
|
74
|
-
message: 'Missing signature',
|
|
75
|
-
hint: 'Include "signature" field with Base64-encoded ECDSA signature',
|
|
76
|
-
status: 400
|
|
77
|
-
},
|
|
78
|
-
[ErrorCodes.INVALID_PUBLIC_KEY]: {
|
|
79
|
-
message: 'Invalid public key',
|
|
80
|
-
hint: 'Public key must be PEM-encoded secp256k1 key',
|
|
81
|
-
status: 400
|
|
82
|
-
},
|
|
83
|
-
[ErrorCodes.INVALID_REQUEST]: {
|
|
84
|
-
message: 'Invalid request format',
|
|
85
|
-
hint: 'Check the API documentation for required fields',
|
|
86
|
-
status: 400
|
|
87
|
-
},
|
|
88
|
-
[ErrorCodes.INTERNAL_ERROR]: {
|
|
89
|
-
message: 'Internal server error',
|
|
90
|
-
hint: 'Please try again later',
|
|
91
|
-
status: 500
|
|
92
|
-
},
|
|
93
|
-
[ErrorCodes.RATE_LIMITED]: {
|
|
94
|
-
message: 'Too many requests',
|
|
95
|
-
hint: 'Please wait before making more requests',
|
|
96
|
-
status: 429
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Create an error response
|
|
102
|
-
* @param {string} code - Error code from ErrorCodes
|
|
103
|
-
* @param {Object} [extra] - Additional fields to include
|
|
104
|
-
* @returns {Object} Error response object
|
|
105
|
-
*/
|
|
106
|
-
export function createError(code, extra = {}) {
|
|
107
|
-
const errorDef = ErrorMessages[code] || ErrorMessages[ErrorCodes.INTERNAL_ERROR];
|
|
108
|
-
|
|
109
|
-
return {
|
|
110
|
-
verified: false,
|
|
111
|
-
error: errorDef.message,
|
|
112
|
-
code,
|
|
113
|
-
hint: errorDef.hint,
|
|
114
|
-
...extra
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Create error response with HTTP status
|
|
120
|
-
* @param {string} code - Error code
|
|
121
|
-
* @param {Object} res - Express response object
|
|
122
|
-
* @param {Object} [extra] - Additional fields
|
|
123
|
-
*/
|
|
124
|
-
export function sendError(code, res, extra = {}) {
|
|
125
|
-
const errorDef = ErrorMessages[code] || ErrorMessages[ErrorCodes.INTERNAL_ERROR];
|
|
126
|
-
res.status(errorDef.status).json(createError(code, extra));
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export default {
|
|
130
|
-
ErrorCodes,
|
|
131
|
-
ErrorMessages,
|
|
132
|
-
createError,
|
|
133
|
-
sendError
|
|
134
|
-
};
|
package/middleware.js
DELETED
|
@@ -1,420 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @aap/server - Express Middleware
|
|
3
|
-
*
|
|
4
|
-
* Drop-in middleware for adding AAP verification to Express apps.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { verify, generateNonce, createProofData } from 'aap-agent-core';
|
|
8
|
-
import {
|
|
9
|
-
generate as generateChallenge,
|
|
10
|
-
generateBatch,
|
|
11
|
-
validateBatch,
|
|
12
|
-
getTypes,
|
|
13
|
-
validate as validateSolution,
|
|
14
|
-
BATCH_SIZE,
|
|
15
|
-
MAX_RESPONSE_TIME_MS,
|
|
16
|
-
CHALLENGE_EXPIRY_MS
|
|
17
|
-
} from './challenges.js';
|
|
18
|
-
import * as logger from './logger.js';
|
|
19
|
-
import { ErrorCodes, sendError } from './errors.js';
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Create AAP verification middleware/router
|
|
23
|
-
*
|
|
24
|
-
* @param {Object} [options]
|
|
25
|
-
* @param {number} [options.challengeExpiryMs=60000] - Challenge expiration time
|
|
26
|
-
* @param {number} [options.maxResponseTimeMs=8000] - Max response time for batch
|
|
27
|
-
* @param {number} [options.batchSize=5] - Number of challenges per batch
|
|
28
|
-
* @param {number} [options.minPassCount] - Minimum challenges to pass (default: all)
|
|
29
|
-
* @param {Function} [options.onVerified] - Callback when agent is verified
|
|
30
|
-
* @param {Function} [options.onFailed] - Callback when verification fails
|
|
31
|
-
* @returns {Function} Express router
|
|
32
|
-
*/
|
|
33
|
-
export function aapMiddleware(options = {}) {
|
|
34
|
-
const {
|
|
35
|
-
challengeExpiryMs = CHALLENGE_EXPIRY_MS,
|
|
36
|
-
maxResponseTimeMs = MAX_RESPONSE_TIME_MS,
|
|
37
|
-
batchSize = BATCH_SIZE,
|
|
38
|
-
minPassCount = null, // null = all must pass
|
|
39
|
-
onVerified,
|
|
40
|
-
onFailed
|
|
41
|
-
} = options;
|
|
42
|
-
|
|
43
|
-
// In-memory challenge store with size limit (DoS protection)
|
|
44
|
-
const MAX_CHALLENGES = 10000;
|
|
45
|
-
const challenges = new Map();
|
|
46
|
-
|
|
47
|
-
// Cleanup expired challenges periodically
|
|
48
|
-
const cleanup = () => {
|
|
49
|
-
const now = Date.now();
|
|
50
|
-
for (const [nonce, challenge] of challenges.entries()) {
|
|
51
|
-
if (now > challenge.expiresAt) {
|
|
52
|
-
challenges.delete(nonce);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Emergency cleanup if still too many (keep newest)
|
|
57
|
-
if (challenges.size > MAX_CHALLENGES) {
|
|
58
|
-
const entries = [...challenges.entries()]
|
|
59
|
-
.sort((a, b) => b[1].timestamp - a[1].timestamp)
|
|
60
|
-
.slice(0, MAX_CHALLENGES / 2);
|
|
61
|
-
challenges.clear();
|
|
62
|
-
entries.forEach(([k, v]) => challenges.set(k, v));
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
// Return a function that creates routes
|
|
67
|
-
return (router) => {
|
|
68
|
-
/**
|
|
69
|
-
* GET /health - Health check
|
|
70
|
-
*/
|
|
71
|
-
router.get('/health', (req, res) => {
|
|
72
|
-
res.json({
|
|
73
|
-
status: 'ok',
|
|
74
|
-
protocol: 'AAP',
|
|
75
|
-
version: '2.0.0',
|
|
76
|
-
mode: 'batch',
|
|
77
|
-
batchSize,
|
|
78
|
-
maxResponseTimeMs,
|
|
79
|
-
challengeTypes: getTypes(),
|
|
80
|
-
activeChallenges: challenges.size
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* POST /challenge - Request a batch of challenges
|
|
86
|
-
*/
|
|
87
|
-
router.post('/challenge', (req, res) => {
|
|
88
|
-
cleanup();
|
|
89
|
-
|
|
90
|
-
const nonce = generateNonce();
|
|
91
|
-
const timestamp = Date.now();
|
|
92
|
-
const { challenges: batchChallenges, validators, expected } = generateBatch(nonce, batchSize);
|
|
93
|
-
|
|
94
|
-
const challengeData = {
|
|
95
|
-
nonce,
|
|
96
|
-
challenges: batchChallenges,
|
|
97
|
-
batchSize,
|
|
98
|
-
timestamp,
|
|
99
|
-
expiresAt: timestamp + challengeExpiryMs,
|
|
100
|
-
maxResponseTimeMs
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
// Store with validators (not sent to client)
|
|
104
|
-
challenges.set(nonce, {
|
|
105
|
-
...challengeData,
|
|
106
|
-
validators,
|
|
107
|
-
expected // For debugging
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// Send without validators
|
|
111
|
-
res.json({
|
|
112
|
-
nonce,
|
|
113
|
-
challenges: batchChallenges,
|
|
114
|
-
batchSize,
|
|
115
|
-
timestamp,
|
|
116
|
-
expiresAt: challengeData.expiresAt,
|
|
117
|
-
maxResponseTimeMs
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* POST /verify - Verify agent's batch solutions
|
|
123
|
-
*/
|
|
124
|
-
router.post('/verify', (req, res) => {
|
|
125
|
-
const {
|
|
126
|
-
solutions,
|
|
127
|
-
signature,
|
|
128
|
-
publicKey,
|
|
129
|
-
publicId,
|
|
130
|
-
nonce,
|
|
131
|
-
timestamp,
|
|
132
|
-
responseTimeMs
|
|
133
|
-
} = req.body;
|
|
134
|
-
|
|
135
|
-
const checks = {
|
|
136
|
-
inputValid: false,
|
|
137
|
-
challengeExists: false,
|
|
138
|
-
notExpired: false,
|
|
139
|
-
solutionsExist: false,
|
|
140
|
-
solutionsValid: false,
|
|
141
|
-
responseTimeValid: false,
|
|
142
|
-
signatureValid: false
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
try {
|
|
146
|
-
// Check 0: Input validation (security)
|
|
147
|
-
if (!nonce || typeof nonce !== 'string' || nonce.length !== 32) {
|
|
148
|
-
return res.status(400).json({ verified: false, error: 'Invalid nonce format', checks });
|
|
149
|
-
}
|
|
150
|
-
if (!publicId || typeof publicId !== 'string' || publicId.length !== 20) {
|
|
151
|
-
return res.status(400).json({ verified: false, error: 'Invalid publicId format', checks });
|
|
152
|
-
}
|
|
153
|
-
if (!signature || typeof signature !== 'string' || signature.length < 50) {
|
|
154
|
-
return res.status(400).json({ verified: false, error: 'Invalid signature format', checks });
|
|
155
|
-
}
|
|
156
|
-
if (!publicKey || typeof publicKey !== 'string' || !publicKey.includes('BEGIN PUBLIC KEY')) {
|
|
157
|
-
return res.status(400).json({ verified: false, error: 'Invalid publicKey format', checks });
|
|
158
|
-
}
|
|
159
|
-
if (!timestamp || typeof timestamp !== 'number') {
|
|
160
|
-
return res.status(400).json({ verified: false, error: 'Invalid timestamp', checks });
|
|
161
|
-
}
|
|
162
|
-
if (!responseTimeMs || typeof responseTimeMs !== 'number' || responseTimeMs < 0) {
|
|
163
|
-
return res.status(400).json({ verified: false, error: 'Invalid responseTimeMs', checks });
|
|
164
|
-
}
|
|
165
|
-
checks.inputValid = true;
|
|
166
|
-
|
|
167
|
-
// Check 1: Challenge exists (check BEFORE delete for race condition fix)
|
|
168
|
-
const challenge = challenges.get(nonce);
|
|
169
|
-
if (!challenge) {
|
|
170
|
-
if (onFailed) onFailed({ error: 'Challenge not found', checks }, req);
|
|
171
|
-
return res.status(400).json({
|
|
172
|
-
verified: false,
|
|
173
|
-
error: 'Challenge not found or already used',
|
|
174
|
-
checks
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
checks.challengeExists = true;
|
|
178
|
-
|
|
179
|
-
// Check 2: Not expired (check BEFORE delete - race condition fix)
|
|
180
|
-
if (Date.now() > challenge.expiresAt) {
|
|
181
|
-
challenges.delete(nonce); // Clean up expired
|
|
182
|
-
if (onFailed) onFailed({ error: 'Challenge expired', checks }, req);
|
|
183
|
-
return res.status(400).json({
|
|
184
|
-
verified: false,
|
|
185
|
-
error: 'Challenge expired',
|
|
186
|
-
checks
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
checks.notExpired = true;
|
|
190
|
-
|
|
191
|
-
// Remove challenge (one-time use) - only after expiry check
|
|
192
|
-
const { validators, batchSize: size } = challenge;
|
|
193
|
-
challenges.delete(nonce);
|
|
194
|
-
|
|
195
|
-
// Check 3: Solutions exist
|
|
196
|
-
if (!solutions || !Array.isArray(solutions) || solutions.length !== size) {
|
|
197
|
-
if (onFailed) onFailed({ error: 'Invalid solutions array', checks }, req);
|
|
198
|
-
return res.status(400).json({
|
|
199
|
-
verified: false,
|
|
200
|
-
error: `Expected ${size} solutions, got ${solutions?.length || 0}`,
|
|
201
|
-
checks
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
checks.solutionsExist = true;
|
|
205
|
-
|
|
206
|
-
// Check 4: Validate all solutions (Proof of Intelligence)
|
|
207
|
-
const batchResult = validateBatch(validators, solutions);
|
|
208
|
-
const requiredPass = minPassCount || size;
|
|
209
|
-
|
|
210
|
-
if (batchResult.passed < requiredPass) {
|
|
211
|
-
if (onFailed) onFailed({ error: 'Solutions validation failed', checks, batchResult }, req);
|
|
212
|
-
return res.status(400).json({
|
|
213
|
-
verified: false,
|
|
214
|
-
error: `Proof of Intelligence failed: ${batchResult.passed}/${size} correct (need ${requiredPass})`,
|
|
215
|
-
checks,
|
|
216
|
-
batchResult
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
checks.solutionsValid = true;
|
|
220
|
-
|
|
221
|
-
// Check 5: Response time (Proof of Liveness) - SERVER-SIDE validation
|
|
222
|
-
const serverResponseTime = Date.now() - challenge.timestamp;
|
|
223
|
-
const effectiveResponseTime = Math.max(responseTimeMs, serverResponseTime);
|
|
224
|
-
|
|
225
|
-
if (effectiveResponseTime > maxResponseTimeMs) {
|
|
226
|
-
if (onFailed) onFailed({ error: 'Response too slow', checks }, req);
|
|
227
|
-
return res.status(400).json({
|
|
228
|
-
verified: false,
|
|
229
|
-
error: `Response too slow: ${effectiveResponseTime}ms > ${maxResponseTimeMs}ms (Proof of Liveness failed)`,
|
|
230
|
-
checks,
|
|
231
|
-
timing: { client: responseTimeMs, server: serverResponseTime }
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
checks.responseTimeValid = true;
|
|
235
|
-
|
|
236
|
-
// Check 6: Signature (Proof of Identity)
|
|
237
|
-
// Sign over solutions array
|
|
238
|
-
const solutionsString = JSON.stringify(solutions);
|
|
239
|
-
const proofData = createProofData({ nonce, solution: solutionsString, publicId, timestamp });
|
|
240
|
-
if (!verify(proofData, signature, publicKey)) {
|
|
241
|
-
if (onFailed) onFailed({ error: 'Invalid signature', checks }, req);
|
|
242
|
-
return res.status(400).json({
|
|
243
|
-
verified: false,
|
|
244
|
-
error: 'Invalid signature (Proof of Identity failed)',
|
|
245
|
-
checks
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
checks.signatureValid = true;
|
|
249
|
-
|
|
250
|
-
// All checks passed
|
|
251
|
-
const result = {
|
|
252
|
-
verified: true,
|
|
253
|
-
role: 'AI_AGENT',
|
|
254
|
-
publicId,
|
|
255
|
-
batchResult,
|
|
256
|
-
responseTimeMs,
|
|
257
|
-
checks
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
if (onVerified) onVerified(result, req);
|
|
261
|
-
|
|
262
|
-
res.json(result);
|
|
263
|
-
|
|
264
|
-
} catch (error) {
|
|
265
|
-
if (onFailed) onFailed({ error: error.message, checks }, req);
|
|
266
|
-
res.status(500).json({
|
|
267
|
-
verified: false,
|
|
268
|
-
error: `Verification error: ${error.message}`,
|
|
269
|
-
checks
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
// ============== Legacy single-challenge endpoints ==============
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* POST /challenge/single - Request a single challenge (legacy)
|
|
278
|
-
*/
|
|
279
|
-
router.post('/challenge/single', (req, res) => {
|
|
280
|
-
cleanup();
|
|
281
|
-
|
|
282
|
-
const nonce = generateNonce();
|
|
283
|
-
const timestamp = Date.now();
|
|
284
|
-
const { type, challenge_string, validate } = generateChallenge(nonce);
|
|
285
|
-
|
|
286
|
-
const challenge = {
|
|
287
|
-
challenge_string,
|
|
288
|
-
nonce,
|
|
289
|
-
type,
|
|
290
|
-
difficulty: 1,
|
|
291
|
-
timestamp,
|
|
292
|
-
expiresAt: timestamp + challengeExpiryMs,
|
|
293
|
-
mode: 'single'
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
challenges.set(nonce, { ...challenge, validate });
|
|
297
|
-
|
|
298
|
-
res.json({
|
|
299
|
-
challenge_string,
|
|
300
|
-
nonce,
|
|
301
|
-
type,
|
|
302
|
-
difficulty: 1,
|
|
303
|
-
timestamp,
|
|
304
|
-
expiresAt: challenge.expiresAt,
|
|
305
|
-
mode: 'single',
|
|
306
|
-
maxResponseTimeMs: 10000 // 10s for single
|
|
307
|
-
});
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* POST /verify/single - Verify single challenge (legacy)
|
|
312
|
-
*/
|
|
313
|
-
router.post('/verify/single', (req, res) => {
|
|
314
|
-
const {
|
|
315
|
-
solution,
|
|
316
|
-
signature,
|
|
317
|
-
publicKey,
|
|
318
|
-
publicId,
|
|
319
|
-
nonce,
|
|
320
|
-
timestamp,
|
|
321
|
-
responseTimeMs
|
|
322
|
-
} = req.body;
|
|
323
|
-
|
|
324
|
-
const checks = {
|
|
325
|
-
challengeExists: false,
|
|
326
|
-
notExpired: false,
|
|
327
|
-
solutionExists: false,
|
|
328
|
-
solutionValid: false,
|
|
329
|
-
responseTimeValid: false,
|
|
330
|
-
signatureValid: false
|
|
331
|
-
};
|
|
332
|
-
|
|
333
|
-
try {
|
|
334
|
-
const challenge = challenges.get(nonce);
|
|
335
|
-
if (!challenge || challenge.mode !== 'single') {
|
|
336
|
-
return res.status(400).json({
|
|
337
|
-
verified: false,
|
|
338
|
-
error: 'Single challenge not found',
|
|
339
|
-
checks
|
|
340
|
-
});
|
|
341
|
-
}
|
|
342
|
-
checks.challengeExists = true;
|
|
343
|
-
|
|
344
|
-
const { validate, type: challengeType } = challenge;
|
|
345
|
-
challenges.delete(nonce);
|
|
346
|
-
|
|
347
|
-
if (Date.now() > challenge.expiresAt) {
|
|
348
|
-
return res.status(400).json({ verified: false, error: 'Challenge expired', checks });
|
|
349
|
-
}
|
|
350
|
-
checks.notExpired = true;
|
|
351
|
-
|
|
352
|
-
if (!solution) {
|
|
353
|
-
return res.status(400).json({ verified: false, error: 'Missing solution', checks });
|
|
354
|
-
}
|
|
355
|
-
checks.solutionExists = true;
|
|
356
|
-
|
|
357
|
-
if (!validate(solution)) {
|
|
358
|
-
return res.status(400).json({ verified: false, error: 'Invalid solution', checks });
|
|
359
|
-
}
|
|
360
|
-
checks.solutionValid = true;
|
|
361
|
-
|
|
362
|
-
if (responseTimeMs > 10000) {
|
|
363
|
-
return res.status(400).json({ verified: false, error: 'Too slow', checks });
|
|
364
|
-
}
|
|
365
|
-
checks.responseTimeValid = true;
|
|
366
|
-
|
|
367
|
-
const proofData = createProofData({ nonce, solution, publicId, timestamp });
|
|
368
|
-
if (!verify(proofData, signature, publicKey)) {
|
|
369
|
-
return res.status(400).json({ verified: false, error: 'Invalid signature', checks });
|
|
370
|
-
}
|
|
371
|
-
checks.signatureValid = true;
|
|
372
|
-
|
|
373
|
-
res.json({
|
|
374
|
-
verified: true,
|
|
375
|
-
role: 'AI_AGENT',
|
|
376
|
-
publicId,
|
|
377
|
-
challengeType,
|
|
378
|
-
checks
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
} catch (error) {
|
|
382
|
-
res.status(500).json({ verified: false, error: error.message, checks });
|
|
383
|
-
}
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
return router;
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* Create a standalone AAP router for Express
|
|
392
|
-
*
|
|
393
|
-
* @param {Object} [options] - Middleware options
|
|
394
|
-
* @returns {Router} Express router
|
|
395
|
-
*
|
|
396
|
-
* @example
|
|
397
|
-
* import express from 'express';
|
|
398
|
-
* import { createRouter } from '@aap/server';
|
|
399
|
-
*
|
|
400
|
-
* const app = express();
|
|
401
|
-
* app.use('/aap/v1', createRouter());
|
|
402
|
-
*/
|
|
403
|
-
export async function createRouter(options = {}) {
|
|
404
|
-
// Dynamic import for optional express dependency
|
|
405
|
-
let express;
|
|
406
|
-
try {
|
|
407
|
-
express = (await import('express')).default;
|
|
408
|
-
} catch {
|
|
409
|
-
throw new Error('express is required for createRouter. Install with: npm install express');
|
|
410
|
-
}
|
|
411
|
-
const router = express.Router();
|
|
412
|
-
router.use(express.json());
|
|
413
|
-
|
|
414
|
-
const middleware = aapMiddleware(options);
|
|
415
|
-
middleware(router);
|
|
416
|
-
|
|
417
|
-
return router;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
export default { aapMiddleware, createRouter };
|
package/ratelimit.js
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AAP Rate Limiter
|
|
3
|
-
*
|
|
4
|
-
* Simple in-memory rate limiting (no external dependencies)
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Create a rate limiter
|
|
9
|
-
* @param {Object} options
|
|
10
|
-
* @param {number} [options.windowMs=60000] - Time window in ms
|
|
11
|
-
* @param {number} [options.max=10] - Max requests per window
|
|
12
|
-
* @param {string} [options.message] - Error message
|
|
13
|
-
* @returns {Function} Express middleware
|
|
14
|
-
*/
|
|
15
|
-
export function createRateLimiter(options = {}) {
|
|
16
|
-
const {
|
|
17
|
-
windowMs = 60000,
|
|
18
|
-
max = 10,
|
|
19
|
-
message = 'Too many requests, please try again later'
|
|
20
|
-
} = options;
|
|
21
|
-
|
|
22
|
-
const requests = new Map();
|
|
23
|
-
|
|
24
|
-
// Cleanup old entries periodically
|
|
25
|
-
setInterval(() => {
|
|
26
|
-
const now = Date.now();
|
|
27
|
-
for (const [key, data] of requests.entries()) {
|
|
28
|
-
if (now - data.firstRequest > windowMs) {
|
|
29
|
-
requests.delete(key);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}, windowMs);
|
|
33
|
-
|
|
34
|
-
return (req, res, next) => {
|
|
35
|
-
const key = req.ip || req.connection.remoteAddress || 'unknown';
|
|
36
|
-
const now = Date.now();
|
|
37
|
-
|
|
38
|
-
let data = requests.get(key);
|
|
39
|
-
|
|
40
|
-
if (!data || now - data.firstRequest > windowMs) {
|
|
41
|
-
// New window
|
|
42
|
-
data = { count: 1, firstRequest: now };
|
|
43
|
-
requests.set(key, data);
|
|
44
|
-
} else {
|
|
45
|
-
data.count++;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Set headers
|
|
49
|
-
const remaining = Math.max(0, max - data.count);
|
|
50
|
-
const resetTime = Math.ceil((data.firstRequest + windowMs) / 1000);
|
|
51
|
-
|
|
52
|
-
res.setHeader('X-RateLimit-Limit', max);
|
|
53
|
-
res.setHeader('X-RateLimit-Remaining', remaining);
|
|
54
|
-
res.setHeader('X-RateLimit-Reset', resetTime);
|
|
55
|
-
|
|
56
|
-
if (data.count > max) {
|
|
57
|
-
res.setHeader('Retry-After', Math.ceil((data.firstRequest + windowMs - now) / 1000));
|
|
58
|
-
return res.status(429).json({
|
|
59
|
-
error: message,
|
|
60
|
-
retryAfter: Math.ceil((data.firstRequest + windowMs - now) / 1000)
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
next();
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Create rate limiter for failed attempts
|
|
70
|
-
* Stricter limits after failures
|
|
71
|
-
*/
|
|
72
|
-
export function createFailureLimiter(options = {}) {
|
|
73
|
-
const {
|
|
74
|
-
windowMs = 60000,
|
|
75
|
-
maxFailures = 5,
|
|
76
|
-
message = 'Too many failed attempts'
|
|
77
|
-
} = options;
|
|
78
|
-
|
|
79
|
-
const failures = new Map();
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
middleware: (req, res, next) => {
|
|
83
|
-
const key = req.ip || req.connection.remoteAddress || 'unknown';
|
|
84
|
-
const data = failures.get(key);
|
|
85
|
-
|
|
86
|
-
if (data && data.count >= maxFailures && Date.now() - data.firstFailure < windowMs) {
|
|
87
|
-
return res.status(429).json({
|
|
88
|
-
error: message,
|
|
89
|
-
retryAfter: Math.ceil((data.firstFailure + windowMs - Date.now()) / 1000)
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
next();
|
|
94
|
-
},
|
|
95
|
-
|
|
96
|
-
recordFailure: (req) => {
|
|
97
|
-
const key = req.ip || req.connection.remoteAddress || 'unknown';
|
|
98
|
-
const now = Date.now();
|
|
99
|
-
let data = failures.get(key);
|
|
100
|
-
|
|
101
|
-
if (!data || now - data.firstFailure > windowMs) {
|
|
102
|
-
data = { count: 1, firstFailure: now };
|
|
103
|
-
} else {
|
|
104
|
-
data.count++;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
failures.set(key, data);
|
|
108
|
-
},
|
|
109
|
-
|
|
110
|
-
clearFailures: (req) => {
|
|
111
|
-
const key = req.ip || req.connection.remoteAddress || 'unknown';
|
|
112
|
-
failures.delete(key);
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export default { createRateLimiter, createFailureLimiter };
|