aap-agent-server 2.7.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +28 -35
- package/package.json +4 -14
- package/websocket.js +229 -236
- 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.1.0';
|
|
28
|
+
export const TOTAL_TIME_MS = 6000;
|
|
29
|
+
export const CHALLENGE_COUNT = 7;
|
|
30
|
+
export const CONNECTION_TIMEOUT_MS = 60000;
|
|
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
|
+
TOTAL_TIME_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.1.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,282 +1,275 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AAP WebSocket Server
|
|
2
|
+
* AAP WebSocket Server v3.1
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Batch challenge delivery over WebSocket.
|
|
5
|
+
* All 7 challenges at once, 6 seconds total.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { WebSocketServer } from 'ws';
|
|
9
|
-
import { randomBytes,
|
|
10
|
-
import { generate as generateChallenge, getTypes } from './challenges.js';
|
|
9
|
+
import { randomBytes, createHash } from 'node:crypto';
|
|
11
10
|
|
|
12
|
-
//
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
11
|
+
// ============== CONSTANTS ==============
|
|
12
|
+
export const PROTOCOL_VERSION = '3.1.0';
|
|
13
|
+
export const CHALLENGE_COUNT = 7;
|
|
14
|
+
export const TOTAL_TIME_MS = 6000;
|
|
15
|
+
export const CONNECTION_TIMEOUT_MS = 60000;
|
|
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 { id: index, type, challenge: q, validate: v };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ============== WEBSOCKET SERVER ==============
|
|
17
114
|
|
|
18
115
|
/**
|
|
19
|
-
* Create AAP WebSocket server
|
|
20
|
-
* @param {Object} options
|
|
21
|
-
* @param {number} [options.port] - Standalone port (if no httpServer)
|
|
22
|
-
* @param {Object} [options.httpServer] - Existing HTTP server
|
|
23
|
-
* @param {string} [options.path='/aap'] - WebSocket path
|
|
24
|
-
* @param {Function} [options.onVerified] - Callback on successful verification
|
|
25
|
-
* @param {Function} [options.onFailed] - Callback on failed verification
|
|
116
|
+
* Create AAP WebSocket verification server
|
|
26
117
|
*/
|
|
27
118
|
export function createAAPWebSocket(options = {}) {
|
|
28
119
|
const {
|
|
29
120
|
port,
|
|
30
|
-
|
|
121
|
+
server,
|
|
31
122
|
path = '/aap',
|
|
32
|
-
onVerified = null,
|
|
33
|
-
onFailed = null,
|
|
34
123
|
challengeCount = CHALLENGE_COUNT,
|
|
35
|
-
|
|
124
|
+
totalTimeMs = TOTAL_TIME_MS,
|
|
125
|
+
connectionTimeoutMs = CONNECTION_TIMEOUT_MS,
|
|
126
|
+
onVerified,
|
|
127
|
+
onFailed
|
|
36
128
|
} = options;
|
|
37
129
|
|
|
38
|
-
const wssOptions =
|
|
39
|
-
? { server: httpServer, path }
|
|
40
|
-
: { port, path };
|
|
41
|
-
|
|
130
|
+
const wssOptions = server ? { server, path } : { port };
|
|
42
131
|
const wss = new WebSocketServer(wssOptions);
|
|
43
|
-
|
|
44
|
-
// Track active sessions
|
|
45
|
-
const sessions = new Map();
|
|
132
|
+
const verifiedTokens = new Map();
|
|
46
133
|
|
|
47
|
-
wss.on('connection', (ws
|
|
134
|
+
wss.on('connection', (ws) => {
|
|
48
135
|
const sessionId = randomBytes(16).toString('hex');
|
|
49
136
|
const nonce = randomBytes(16).toString('hex');
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
currentChallenge: 0,
|
|
58
|
-
challenges: [],
|
|
59
|
-
validators: [],
|
|
60
|
-
answers: [],
|
|
61
|
-
timings: [],
|
|
62
|
-
publicKey: null,
|
|
63
|
-
publicId: null,
|
|
64
|
-
challengeStartTime: null,
|
|
65
|
-
timeout: null,
|
|
66
|
-
connectionTimeout: null
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
sessions.set(sessionId, session);
|
|
70
|
-
|
|
71
|
-
// Connection timeout
|
|
72
|
-
session.connectionTimeout = setTimeout(() => {
|
|
73
|
-
sendError(ws, 'Connection timeout');
|
|
74
|
-
ws.close();
|
|
75
|
-
}, CONNECTION_TIMEOUT_MS);
|
|
76
|
-
|
|
77
|
-
// Generate all challenges upfront (but send one at a time)
|
|
78
|
-
const types = getTypes();
|
|
137
|
+
let challenges = [];
|
|
138
|
+
let validators = [];
|
|
139
|
+
let challengesSentAt = null;
|
|
140
|
+
let publicId = null;
|
|
141
|
+
let answered = false;
|
|
142
|
+
|
|
143
|
+
// Generate challenges
|
|
79
144
|
for (let i = 0; i < challengeCount; i++) {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
id: i,
|
|
84
|
-
type,
|
|
85
|
-
challenge_string: challenge.challenge_string
|
|
86
|
-
});
|
|
87
|
-
session.validators.push(challenge.validate);
|
|
145
|
+
const ch = generateChallenge(nonce, i);
|
|
146
|
+
challenges.push({ id: ch.id, type: ch.type, challenge: ch.challenge });
|
|
147
|
+
validators.push(ch.validate);
|
|
88
148
|
}
|
|
89
|
-
|
|
149
|
+
|
|
150
|
+
// Connection timeout
|
|
151
|
+
const connTimer = setTimeout(() => {
|
|
152
|
+
send(ws, { type: 'error', code: 'TIMEOUT', message: 'Connection timeout' });
|
|
153
|
+
ws.close();
|
|
154
|
+
}, connectionTimeoutMs);
|
|
155
|
+
|
|
90
156
|
// Send handshake
|
|
91
157
|
send(ws, {
|
|
92
158
|
type: 'handshake',
|
|
93
159
|
sessionId,
|
|
94
|
-
nonce,
|
|
95
160
|
protocol: 'AAP',
|
|
96
161
|
version: PROTOCOL_VERSION,
|
|
97
|
-
mode: '
|
|
162
|
+
mode: 'batch',
|
|
98
163
|
challengeCount,
|
|
99
|
-
|
|
100
|
-
message: '
|
|
164
|
+
totalTimeMs,
|
|
165
|
+
message: 'Send {"type":"ready"} to receive challenges.'
|
|
101
166
|
});
|
|
102
|
-
|
|
167
|
+
|
|
103
168
|
ws.on('message', (data) => {
|
|
169
|
+
let msg;
|
|
104
170
|
try {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
sendError(ws, 'Invalid JSON');
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
ws.on('close', () => {
|
|
113
|
-
clearTimeout(session.timeout);
|
|
114
|
-
clearTimeout(session.connectionTimeout);
|
|
115
|
-
sessions.delete(sessionId);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
ws.on('error', () => {
|
|
119
|
-
sessions.delete(sessionId);
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
return {
|
|
124
|
-
wss,
|
|
125
|
-
sessions,
|
|
126
|
-
close: () => wss.close()
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function send(ws, data) {
|
|
131
|
-
if (ws.readyState === 1) { // OPEN
|
|
132
|
-
ws.send(JSON.stringify(data));
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function sendError(ws, message, code = 'ERROR') {
|
|
137
|
-
send(ws, { type: 'error', code, message });
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function handleMessage(session, msg, options) {
|
|
141
|
-
const { ws, currentChallenge, challenges, validators, answers, timings } = session;
|
|
142
|
-
const { onVerified, onFailed, timePerChallengeMs } = options;
|
|
143
|
-
|
|
144
|
-
switch (msg.type) {
|
|
145
|
-
case 'ready':
|
|
146
|
-
// Client ready to start, optionally provides identity
|
|
147
|
-
if (currentChallenge !== 0) {
|
|
148
|
-
sendError(ws, 'Already started');
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
session.publicKey = msg.publicKey || null;
|
|
153
|
-
session.publicId = msg.publicId || null;
|
|
154
|
-
|
|
155
|
-
// Send first challenge
|
|
156
|
-
sendNextChallenge(session, timePerChallengeMs);
|
|
157
|
-
break;
|
|
158
|
-
|
|
159
|
-
case 'answer':
|
|
160
|
-
// Client submitting answer
|
|
161
|
-
if (session.challengeStartTime === null) {
|
|
162
|
-
sendError(ws, 'No active challenge');
|
|
171
|
+
msg = JSON.parse(data.toString());
|
|
172
|
+
} catch {
|
|
173
|
+
send(ws, { type: 'error', code: 'INVALID_JSON', message: 'Invalid JSON' });
|
|
163
174
|
return;
|
|
164
175
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
176
|
+
|
|
177
|
+
if (msg.type === 'ready' && !challengesSentAt) {
|
|
178
|
+
publicId = msg.publicId || 'anon-' + randomBytes(4).toString('hex');
|
|
179
|
+
challengesSentAt = Date.now();
|
|
180
|
+
|
|
181
|
+
// Send all challenges at once
|
|
171
182
|
send(ws, {
|
|
172
|
-
type: '
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
183
|
+
type: 'challenges',
|
|
184
|
+
nonce,
|
|
185
|
+
challenges,
|
|
186
|
+
totalTimeMs,
|
|
187
|
+
expiresAt: challengesSentAt + totalTimeMs
|
|
176
188
|
});
|
|
177
|
-
finishSession(session, false, 'Too slow on challenge ' + currentChallenge, { onVerified, onFailed });
|
|
178
|
-
return;
|
|
179
189
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const valid = validators[currentChallenge](msg.answer);
|
|
187
|
-
|
|
188
|
-
send(ws, {
|
|
189
|
-
type: 'ack',
|
|
190
|
-
challengeId: currentChallenge,
|
|
191
|
-
responseTimeMs: responseTime,
|
|
192
|
-
valid // Real-time feedback
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
session.currentChallenge++;
|
|
196
|
-
|
|
197
|
-
// More challenges?
|
|
198
|
-
if (session.currentChallenge < challenges.length) {
|
|
199
|
-
// Small delay then next challenge
|
|
200
|
-
setTimeout(() => sendNextChallenge(session, timePerChallengeMs), 100);
|
|
201
|
-
} else {
|
|
202
|
-
// All done - calculate result
|
|
203
|
-
const passed = answers.filter((_, i) => validators[i](answers[i])).length;
|
|
204
|
-
const allPassed = passed === challenges.length;
|
|
205
|
-
const totalTime = Date.now() - session.startTime;
|
|
190
|
+
else if (msg.type === 'answers' && challengesSentAt && !answered) {
|
|
191
|
+
answered = true;
|
|
192
|
+
clearTimeout(connTimer);
|
|
193
|
+
|
|
194
|
+
const elapsed = Date.now() - challengesSentAt;
|
|
195
|
+
const answers = msg.answers || [];
|
|
206
196
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
197
|
+
// Too slow?
|
|
198
|
+
if (elapsed > totalTimeMs) {
|
|
199
|
+
const result = {
|
|
200
|
+
type: 'result',
|
|
201
|
+
verified: false,
|
|
202
|
+
message: `Too slow: ${elapsed}ms > ${totalTimeMs}ms`,
|
|
203
|
+
publicId,
|
|
204
|
+
responseTimeMs: elapsed
|
|
205
|
+
};
|
|
206
|
+
if (onFailed) onFailed(result);
|
|
207
|
+
send(ws, result);
|
|
208
|
+
setTimeout(() => ws.close(), 300);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Validate all answers
|
|
213
|
+
let passed = 0;
|
|
214
|
+
const results = [];
|
|
215
|
+
for (let i = 0; i < challengeCount; i++) {
|
|
216
|
+
const valid = validators[i](answers[i]);
|
|
217
|
+
if (valid) passed++;
|
|
218
|
+
results.push({ id: i, valid });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const success = passed === challengeCount;
|
|
222
|
+
const result = {
|
|
223
|
+
type: 'result',
|
|
224
|
+
verified: success,
|
|
225
|
+
message: success ? 'All challenges passed' : `Failed: ${passed}/${challengeCount}`,
|
|
226
|
+
publicId,
|
|
210
227
|
passed,
|
|
211
|
-
total:
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
228
|
+
total: challengeCount,
|
|
229
|
+
results,
|
|
230
|
+
responseTimeMs: elapsed
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
if (success) {
|
|
234
|
+
result.role = 'AI_AGENT';
|
|
235
|
+
result.sessionToken = randomBytes(32).toString('hex');
|
|
236
|
+
|
|
237
|
+
verifiedTokens.set(result.sessionToken, {
|
|
238
|
+
publicId,
|
|
239
|
+
nonce,
|
|
240
|
+
verifiedAt: Date.now(),
|
|
241
|
+
expiresAt: Date.now() + 3600000,
|
|
242
|
+
responseTimeMs: elapsed
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
if (onVerified) onVerified(result);
|
|
246
|
+
} else {
|
|
247
|
+
if (onFailed) onFailed(result);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
send(ws, result);
|
|
251
|
+
setTimeout(() => ws.close(), 300);
|
|
215
252
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
default:
|
|
219
|
-
sendError(ws, 'Unknown message type: ' + msg.type);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
253
|
+
});
|
|
222
254
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const challenge = challenges[currentChallenge];
|
|
226
|
-
|
|
227
|
-
session.challengeStartTime = Date.now();
|
|
228
|
-
|
|
229
|
-
send(ws, {
|
|
230
|
-
type: 'challenge',
|
|
231
|
-
id: currentChallenge,
|
|
232
|
-
total: challenges.length,
|
|
233
|
-
challenge: challenge.challenge_string,
|
|
234
|
-
timeLimit: timePerChallengeMs
|
|
255
|
+
ws.on('close', () => clearTimeout(connTimer));
|
|
256
|
+
ws.on('error', () => clearTimeout(connTimer));
|
|
235
257
|
});
|
|
236
|
-
|
|
237
|
-
// Timeout for this challenge
|
|
238
|
-
session.timeout = setTimeout(() => {
|
|
239
|
-
send(ws, {
|
|
240
|
-
type: 'timeout',
|
|
241
|
-
challengeId: currentChallenge,
|
|
242
|
-
message: 'Time expired'
|
|
243
|
-
});
|
|
244
|
-
finishSession(session, false, 'Timeout on challenge ' + currentChallenge, {});
|
|
245
|
-
}, timePerChallengeMs + 100); // Small grace period for network
|
|
246
|
-
}
|
|
247
258
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
verified: success,
|
|
258
|
-
nonce,
|
|
259
|
-
publicId,
|
|
260
|
-
message,
|
|
261
|
-
passed,
|
|
262
|
-
total,
|
|
263
|
-
timings,
|
|
264
|
-
totalTimeMs: totalTime || (Date.now() - startTime),
|
|
265
|
-
avgResponseMs: timings?.length ? Math.round(timings.reduce((a, b) => a + b, 0) / timings.length) : null
|
|
259
|
+
return {
|
|
260
|
+
wss,
|
|
261
|
+
verifiedTokens,
|
|
262
|
+
close: () => wss.close(),
|
|
263
|
+
isVerified: (token) => {
|
|
264
|
+
const session = verifiedTokens.get(token);
|
|
265
|
+
return session && Date.now() < session.expiresAt;
|
|
266
|
+
},
|
|
267
|
+
getSession: (token) => verifiedTokens.get(token)
|
|
266
268
|
};
|
|
267
|
-
|
|
268
|
-
if (success) {
|
|
269
|
-
result.role = 'AI_AGENT';
|
|
270
|
-
result.sessionToken = randomBytes(32).toString('hex');
|
|
271
|
-
if (onVerified) onVerified(result, session);
|
|
272
|
-
} else {
|
|
273
|
-
if (onFailed) onFailed(result, session);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
send(ws, result);
|
|
277
|
-
|
|
278
|
-
// Close after sending result
|
|
279
|
-
setTimeout(() => ws.close(), 500);
|
|
280
269
|
}
|
|
281
270
|
|
|
282
|
-
|
|
271
|
+
function send(ws, data) {
|
|
272
|
+
if (ws.readyState === 1) ws.send(JSON.stringify(data));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export default { createAAPWebSocket, PROTOCOL_VERSION, CHALLENGE_COUNT, TOTAL_TIME_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 };
|