aap-agent-server 2.7.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 +28 -35
- package/package.json +4 -14
- package/websocket.js +244 -160
- package/errors.js +0 -134
- package/middleware.js +0 -420
- package/ratelimit.js +0 -117
package/index.js
CHANGED
|
@@ -1,46 +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
|
-
|
|
14
|
-
|
|
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';
|
|
15
24
|
export * as logger from './logger.js';
|
|
16
25
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
import { createAAPWebSocket } from './websocket.js';
|
|
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;
|
|
23
31
|
|
|
24
|
-
|
|
32
|
+
import { createAAPWebSocket } from './websocket.js';
|
|
25
33
|
|
|
26
34
|
export default {
|
|
27
|
-
// Core
|
|
28
|
-
aapMiddleware,
|
|
29
|
-
createRouter,
|
|
30
|
-
challenges,
|
|
31
|
-
|
|
32
|
-
// WebSocket (v2.7+)
|
|
33
35
|
createAAPWebSocket,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
createFailureLimiter,
|
|
38
|
-
createWhitelist,
|
|
39
|
-
createKeyRotation,
|
|
40
|
-
|
|
41
|
-
// Persistence
|
|
42
|
-
createStore,
|
|
43
|
-
createMemoryStore,
|
|
44
|
-
createFileStore,
|
|
45
|
-
createRedisStore
|
|
36
|
+
PROTOCOL_VERSION,
|
|
37
|
+
TIME_PER_CHALLENGE_MS,
|
|
38
|
+
CHALLENGE_COUNT
|
|
46
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,17 +20,8 @@
|
|
|
21
20
|
},
|
|
22
21
|
"homepage": "https://github.com/ira-hash/agent-attestation-protocol#readme",
|
|
23
22
|
"dependencies": {
|
|
24
|
-
"aap-agent-core": "^2.6.0",
|
|
25
23
|
"ws": "^8.16.0"
|
|
26
24
|
},
|
|
27
|
-
"peerDependencies": {
|
|
28
|
-
"express": "^4.18.0 || ^5.0.0"
|
|
29
|
-
},
|
|
30
|
-
"peerDependenciesMeta": {
|
|
31
|
-
"express": {
|
|
32
|
-
"optional": true
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
25
|
"engines": {
|
|
36
26
|
"node": ">=18.0.0"
|
|
37
27
|
}
|
package/websocket.js
CHANGED
|
@@ -1,121 +1,215 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AAP WebSocket Server
|
|
2
|
+
* AAP WebSocket Server v3.0
|
|
3
3
|
*
|
|
4
4
|
* Sequential challenge delivery over persistent connection.
|
|
5
|
-
*
|
|
5
|
+
* No preview. Server controls pacing. Humans cannot pass.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { WebSocketServer } from 'ws';
|
|
9
|
-
import { randomBytes, createVerify } from 'node:crypto';
|
|
10
|
-
import { generate as generateChallenge, getTypes } from './challenges.js';
|
|
9
|
+
import { randomBytes, createHash, createVerify } from 'node:crypto';
|
|
11
10
|
|
|
12
|
-
//
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
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 ==============
|
|
17
114
|
|
|
18
115
|
/**
|
|
19
|
-
* Create AAP WebSocket server
|
|
116
|
+
* Create AAP WebSocket verification server
|
|
20
117
|
* @param {Object} options
|
|
21
|
-
* @param {number} [options.port] -
|
|
22
|
-
* @param {Object} [options.
|
|
118
|
+
* @param {number} [options.port] - Port for standalone server
|
|
119
|
+
* @param {Object} [options.server] - Existing HTTP server to attach to
|
|
23
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
|
|
24
123
|
* @param {Function} [options.onVerified] - Callback on successful verification
|
|
25
124
|
* @param {Function} [options.onFailed] - Callback on failed verification
|
|
125
|
+
* @returns {Object} { wss, sessions, close }
|
|
26
126
|
*/
|
|
27
127
|
export function createAAPWebSocket(options = {}) {
|
|
28
128
|
const {
|
|
29
129
|
port,
|
|
30
|
-
|
|
130
|
+
server,
|
|
31
131
|
path = '/aap',
|
|
32
|
-
onVerified = null,
|
|
33
|
-
onFailed = null,
|
|
34
132
|
challengeCount = CHALLENGE_COUNT,
|
|
35
|
-
timePerChallengeMs = TIME_PER_CHALLENGE_MS
|
|
133
|
+
timePerChallengeMs = TIME_PER_CHALLENGE_MS,
|
|
134
|
+
connectionTimeoutMs = CONNECTION_TIMEOUT_MS,
|
|
135
|
+
onVerified,
|
|
136
|
+
onFailed
|
|
36
137
|
} = options;
|
|
37
138
|
|
|
38
|
-
const wssOptions =
|
|
39
|
-
? { server: httpServer, path }
|
|
40
|
-
: { port, path };
|
|
41
|
-
|
|
139
|
+
const wssOptions = server ? { server, path } : { port };
|
|
42
140
|
const wss = new WebSocketServer(wssOptions);
|
|
43
|
-
|
|
44
|
-
// Track active sessions
|
|
45
141
|
const sessions = new Map();
|
|
142
|
+
const verifiedTokens = new Map();
|
|
46
143
|
|
|
47
|
-
wss.on('connection', (ws
|
|
144
|
+
wss.on('connection', (ws) => {
|
|
48
145
|
const sessionId = randomBytes(16).toString('hex');
|
|
49
146
|
const nonce = randomBytes(16).toString('hex');
|
|
50
147
|
const startTime = Date.now();
|
|
51
|
-
|
|
148
|
+
|
|
52
149
|
const session = {
|
|
53
150
|
id: sessionId,
|
|
54
|
-
nonce,
|
|
55
151
|
ws,
|
|
152
|
+
nonce,
|
|
56
153
|
startTime,
|
|
57
|
-
|
|
154
|
+
current: 0,
|
|
58
155
|
challenges: [],
|
|
59
|
-
validators: [],
|
|
60
156
|
answers: [],
|
|
61
157
|
timings: [],
|
|
62
|
-
publicKey: null,
|
|
63
158
|
publicId: null,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
159
|
+
challengeStart: null,
|
|
160
|
+
timer: null,
|
|
161
|
+
connTimer: null
|
|
67
162
|
};
|
|
68
|
-
|
|
163
|
+
|
|
69
164
|
sessions.set(sessionId, session);
|
|
70
|
-
|
|
71
|
-
//
|
|
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();
|
|
165
|
+
|
|
166
|
+
// Generate challenges
|
|
79
167
|
for (let i = 0; i < challengeCount; i++) {
|
|
80
|
-
|
|
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);
|
|
168
|
+
session.challenges.push(generateChallenge(nonce, i));
|
|
88
169
|
}
|
|
89
|
-
|
|
170
|
+
|
|
171
|
+
// Connection timeout
|
|
172
|
+
session.connTimer = setTimeout(() => {
|
|
173
|
+
send(ws, { type: 'error', code: 'TIMEOUT', message: 'Connection timeout' });
|
|
174
|
+
ws.close();
|
|
175
|
+
}, connectionTimeoutMs);
|
|
176
|
+
|
|
90
177
|
// Send handshake
|
|
91
178
|
send(ws, {
|
|
92
179
|
type: 'handshake',
|
|
93
180
|
sessionId,
|
|
94
|
-
nonce,
|
|
95
181
|
protocol: 'AAP',
|
|
96
182
|
version: PROTOCOL_VERSION,
|
|
97
|
-
mode: 'websocket',
|
|
98
183
|
challengeCount,
|
|
99
184
|
timePerChallengeMs,
|
|
100
|
-
message: '
|
|
185
|
+
message: 'Send {"type":"ready"} to begin verification.'
|
|
101
186
|
});
|
|
102
|
-
|
|
187
|
+
|
|
103
188
|
ws.on('message', (data) => {
|
|
189
|
+
let msg;
|
|
104
190
|
try {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
191
|
+
msg = JSON.parse(data.toString());
|
|
192
|
+
} catch {
|
|
193
|
+
send(ws, { type: 'error', code: 'INVALID_JSON', message: 'Invalid JSON' });
|
|
194
|
+
return;
|
|
109
195
|
}
|
|
196
|
+
|
|
197
|
+
handleMessage(session, msg, {
|
|
198
|
+
challengeCount,
|
|
199
|
+
timePerChallengeMs,
|
|
200
|
+
onVerified,
|
|
201
|
+
onFailed,
|
|
202
|
+
verifiedTokens
|
|
203
|
+
});
|
|
110
204
|
});
|
|
111
|
-
|
|
205
|
+
|
|
112
206
|
ws.on('close', () => {
|
|
113
|
-
|
|
114
|
-
clearTimeout(session.connectionTimeout);
|
|
207
|
+
cleanup(session);
|
|
115
208
|
sessions.delete(sessionId);
|
|
116
209
|
});
|
|
117
|
-
|
|
210
|
+
|
|
118
211
|
ws.on('error', () => {
|
|
212
|
+
cleanup(session);
|
|
119
213
|
sessions.delete(sessionId);
|
|
120
214
|
});
|
|
121
215
|
});
|
|
@@ -123,160 +217,150 @@ export function createAAPWebSocket(options = {}) {
|
|
|
123
217
|
return {
|
|
124
218
|
wss,
|
|
125
219
|
sessions,
|
|
126
|
-
|
|
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)
|
|
127
231
|
};
|
|
128
232
|
}
|
|
129
233
|
|
|
130
234
|
function send(ws, data) {
|
|
131
|
-
if (ws.readyState === 1) {
|
|
235
|
+
if (ws.readyState === 1) {
|
|
132
236
|
ws.send(JSON.stringify(data));
|
|
133
237
|
}
|
|
134
238
|
}
|
|
135
239
|
|
|
136
|
-
function
|
|
137
|
-
|
|
240
|
+
function cleanup(session) {
|
|
241
|
+
clearTimeout(session.timer);
|
|
242
|
+
clearTimeout(session.connTimer);
|
|
138
243
|
}
|
|
139
244
|
|
|
140
245
|
function handleMessage(session, msg, options) {
|
|
141
|
-
const { ws,
|
|
142
|
-
const { onVerified, onFailed,
|
|
246
|
+
const { ws, current, challenges, answers, timings } = session;
|
|
247
|
+
const { challengeCount, timePerChallengeMs, onVerified, onFailed, verifiedTokens } = options;
|
|
143
248
|
|
|
144
249
|
switch (msg.type) {
|
|
145
250
|
case 'ready':
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
sendError(ws, 'Already started');
|
|
251
|
+
if (current !== 0) {
|
|
252
|
+
send(ws, { type: 'error', code: 'ALREADY_STARTED', message: 'Already started' });
|
|
149
253
|
return;
|
|
150
254
|
}
|
|
151
|
-
|
|
152
|
-
session.publicKey = msg.publicKey || null;
|
|
153
|
-
session.publicId = msg.publicId || null;
|
|
154
|
-
|
|
155
|
-
// Send first challenge
|
|
255
|
+
session.publicId = msg.publicId || 'anon-' + randomBytes(4).toString('hex');
|
|
156
256
|
sendNextChallenge(session, timePerChallengeMs);
|
|
157
257
|
break;
|
|
158
|
-
|
|
258
|
+
|
|
159
259
|
case 'answer':
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
sendError(ws, 'No active challenge');
|
|
260
|
+
if (session.challengeStart === null) {
|
|
261
|
+
send(ws, { type: 'error', code: 'NO_CHALLENGE', message: 'No active challenge' });
|
|
163
262
|
return;
|
|
164
263
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
//
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
type: 'timeout',
|
|
173
|
-
challengeId: currentChallenge,
|
|
174
|
-
responseTimeMs: responseTime,
|
|
175
|
-
limit: timePerChallengeMs
|
|
176
|
-
});
|
|
177
|
-
finishSession(session, false, 'Too slow on challenge ' + currentChallenge, { onVerified, onFailed });
|
|
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);
|
|
178
271
|
return;
|
|
179
272
|
}
|
|
180
|
-
|
|
181
|
-
// Record answer
|
|
273
|
+
|
|
274
|
+
// Record answer
|
|
182
275
|
answers.push(msg.answer);
|
|
183
|
-
timings.push(
|
|
184
|
-
|
|
185
|
-
// Validate
|
|
186
|
-
const valid =
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
responseTimeMs: responseTime,
|
|
192
|
-
valid // Real-time feedback
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
session.currentChallenge++;
|
|
196
|
-
|
|
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
|
+
|
|
197
284
|
// More challenges?
|
|
198
|
-
if (session.
|
|
199
|
-
|
|
200
|
-
setTimeout(() => sendNextChallenge(session, timePerChallengeMs), 100);
|
|
285
|
+
if (session.current < challengeCount) {
|
|
286
|
+
setTimeout(() => sendNextChallenge(session, timePerChallengeMs), 50);
|
|
201
287
|
} else {
|
|
202
|
-
//
|
|
203
|
-
const passed = answers.filter((
|
|
204
|
-
const
|
|
205
|
-
|
|
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
|
-
});
|
|
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);
|
|
215
292
|
}
|
|
216
293
|
break;
|
|
217
|
-
|
|
294
|
+
|
|
218
295
|
default:
|
|
219
|
-
|
|
296
|
+
send(ws, { type: 'error', code: 'UNKNOWN_TYPE', message: 'Unknown message type' });
|
|
220
297
|
}
|
|
221
298
|
}
|
|
222
299
|
|
|
223
300
|
function sendNextChallenge(session, timePerChallengeMs) {
|
|
224
|
-
const { ws,
|
|
225
|
-
const challenge = challenges[
|
|
226
|
-
|
|
227
|
-
session.
|
|
228
|
-
|
|
301
|
+
const { ws, current, challenges } = session;
|
|
302
|
+
const challenge = challenges[current];
|
|
303
|
+
|
|
304
|
+
session.challengeStart = Date.now();
|
|
305
|
+
|
|
229
306
|
send(ws, {
|
|
230
307
|
type: 'challenge',
|
|
231
|
-
id:
|
|
308
|
+
id: current,
|
|
232
309
|
total: challenges.length,
|
|
233
|
-
challenge: challenge.
|
|
310
|
+
challenge: challenge.challenge,
|
|
234
311
|
timeLimit: timePerChallengeMs
|
|
235
312
|
});
|
|
236
|
-
|
|
313
|
+
|
|
237
314
|
// Timeout for this challenge
|
|
238
|
-
session.
|
|
239
|
-
send(ws, {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
message: 'Time expired'
|
|
243
|
-
});
|
|
244
|
-
finishSession(session, false, 'Timeout on challenge ' + currentChallenge, {});
|
|
245
|
-
}, timePerChallengeMs + 100); // Small grace period for network
|
|
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);
|
|
246
319
|
}
|
|
247
320
|
|
|
248
|
-
function finishSession(session, success, message, options) {
|
|
249
|
-
const { ws, nonce, publicId, startTime } = session;
|
|
250
|
-
const { onVerified, onFailed,
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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;
|
|
255
328
|
const result = {
|
|
256
329
|
type: 'result',
|
|
257
330
|
verified: success,
|
|
331
|
+
message,
|
|
258
332
|
nonce,
|
|
259
333
|
publicId,
|
|
260
|
-
message,
|
|
261
334
|
passed,
|
|
262
|
-
total,
|
|
335
|
+
total: session.challenges.length,
|
|
263
336
|
timings,
|
|
264
|
-
totalTimeMs: totalTime
|
|
265
|
-
avgResponseMs: timings
|
|
337
|
+
totalTimeMs: totalTime,
|
|
338
|
+
avgResponseMs: timings.length ? Math.round(timings.reduce((a, b) => a + b, 0) / timings.length) : null
|
|
266
339
|
};
|
|
267
|
-
|
|
340
|
+
|
|
268
341
|
if (success) {
|
|
269
342
|
result.role = 'AI_AGENT';
|
|
270
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
|
+
|
|
271
357
|
if (onVerified) onVerified(result, session);
|
|
272
358
|
} else {
|
|
273
359
|
if (onFailed) onFailed(result, session);
|
|
274
360
|
}
|
|
275
|
-
|
|
361
|
+
|
|
276
362
|
send(ws, result);
|
|
277
|
-
|
|
278
|
-
// Close after sending result
|
|
279
|
-
setTimeout(() => ws.close(), 500);
|
|
363
|
+
setTimeout(() => ws.close(), 300);
|
|
280
364
|
}
|
|
281
365
|
|
|
282
|
-
export default { createAAPWebSocket };
|
|
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 };
|