@highway1/core 0.1.10 → 0.1.13
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/dist/index.js +23 -22
- package/dist/index.js.map +1 -1
- package/package.json +17 -4
- package/src/discovery/agent-card-encoder.ts +119 -0
- package/src/discovery/agent-card-schema.ts +87 -0
- package/src/discovery/agent-card-types.ts +99 -0
- package/src/discovery/agent-card.ts +190 -0
- package/src/discovery/bootstrap.ts +63 -0
- package/src/discovery/capability-matcher.ts +167 -0
- package/src/discovery/dht.ts +236 -0
- package/src/discovery/index.ts +3 -0
- package/src/discovery/search-index.ts +247 -0
- package/src/discovery/semantic-search.ts +218 -0
- package/src/identity/did.ts +48 -0
- package/src/identity/document.ts +77 -0
- package/src/identity/index.ts +4 -0
- package/src/identity/keys.ts +79 -0
- package/src/identity/signer.ts +55 -0
- package/src/index.ts +33 -0
- package/src/messaging/codec.ts +47 -0
- package/src/messaging/envelope.ts +107 -0
- package/src/messaging/index.ts +3 -0
- package/src/messaging/router.ts +307 -0
- package/src/transport/connection.ts +77 -0
- package/src/transport/index.ts +2 -0
- package/src/transport/node.ts +127 -0
- package/src/trust/endorsement.ts +167 -0
- package/src/trust/index.ts +194 -0
- package/src/trust/interaction-history.ts +155 -0
- package/src/trust/sybil-defense.ts +232 -0
- package/src/trust/trust-score.ts +136 -0
- package/src/utils/errors.ts +38 -0
- package/src/utils/logger.ts +48 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import type { Libp2p } from 'libp2p';
|
|
2
|
+
import { peerIdFromString } from '@libp2p/peer-id';
|
|
3
|
+
import { multiaddr } from '@multiformats/multiaddr';
|
|
4
|
+
import type { MessageEnvelope } from './envelope.js';
|
|
5
|
+
import type { DHTOperations } from '../discovery/dht.js';
|
|
6
|
+
import { encodeMessage, decodeMessage } from './codec.js';
|
|
7
|
+
import { validateEnvelope } from './envelope.js';
|
|
8
|
+
import { createLogger } from '../utils/logger.js';
|
|
9
|
+
import { MessagingError } from '../utils/errors.js';
|
|
10
|
+
|
|
11
|
+
const logger = createLogger('router');
|
|
12
|
+
|
|
13
|
+
export type MessageHandler = (
|
|
14
|
+
envelope: MessageEnvelope
|
|
15
|
+
) => Promise<MessageEnvelope | void>;
|
|
16
|
+
|
|
17
|
+
export interface PeerHint {
|
|
18
|
+
peerId: string;
|
|
19
|
+
multiaddrs: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface MessageRouter {
|
|
23
|
+
registerHandler: (protocol: string, handler: MessageHandler) => void;
|
|
24
|
+
unregisterHandler: (protocol: string) => void;
|
|
25
|
+
registerCatchAllHandler: (handler: MessageHandler) => void;
|
|
26
|
+
sendMessage: (envelope: MessageEnvelope, peerHint?: PeerHint) => Promise<MessageEnvelope | void>;
|
|
27
|
+
start: () => Promise<void>;
|
|
28
|
+
stop: () => Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create a message router for a libp2p node.
|
|
33
|
+
* When dht is provided, sendMessage can resolve DIDs to peer addresses via DHT lookup.
|
|
34
|
+
* When relayPeers is provided, sendMessage will attempt relay fallback if direct dial fails.
|
|
35
|
+
*/
|
|
36
|
+
export function createMessageRouter(
|
|
37
|
+
libp2p: Libp2p,
|
|
38
|
+
verifyFn: (signature: Uint8Array, data: Uint8Array) => Promise<boolean>,
|
|
39
|
+
dht?: DHTOperations,
|
|
40
|
+
relayPeers?: string[]
|
|
41
|
+
): MessageRouter {
|
|
42
|
+
const handlers = new Map<string, MessageHandler>();
|
|
43
|
+
let catchAllHandler: MessageHandler | undefined;
|
|
44
|
+
const PROTOCOL_PREFIX = '/clawiverse/msg/1.0.0';
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
registerHandler: (protocol: string, handler: MessageHandler) => {
|
|
48
|
+
handlers.set(protocol, handler);
|
|
49
|
+
logger.info('Registered message handler', { protocol });
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
unregisterHandler: (protocol: string) => {
|
|
53
|
+
handlers.delete(protocol);
|
|
54
|
+
logger.info('Unregistered message handler', { protocol });
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
registerCatchAllHandler: (handler: MessageHandler) => {
|
|
58
|
+
catchAllHandler = handler;
|
|
59
|
+
logger.info('Registered catch-all message handler');
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
sendMessage: async (envelope: MessageEnvelope, peerHint?: PeerHint) => {
|
|
63
|
+
try {
|
|
64
|
+
if (!validateEnvelope(envelope)) {
|
|
65
|
+
throw new MessagingError('Invalid message envelope');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Resolve peer: try peerHint first, then DHT
|
|
69
|
+
let targetPeerIdStr: string | undefined;
|
|
70
|
+
let targetMultiaddrs: string[] = [];
|
|
71
|
+
|
|
72
|
+
if (peerHint) {
|
|
73
|
+
targetPeerIdStr = peerHint.peerId;
|
|
74
|
+
targetMultiaddrs = peerHint.multiaddrs;
|
|
75
|
+
logger.info('Using peer hint for direct addressing', { peerId: targetPeerIdStr });
|
|
76
|
+
} else if (dht) {
|
|
77
|
+
const resolved = await dht.resolveDID(envelope.to);
|
|
78
|
+
if (resolved) {
|
|
79
|
+
targetPeerIdStr = resolved.peerId;
|
|
80
|
+
targetMultiaddrs = resolved.multiaddrs;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!targetPeerIdStr) {
|
|
85
|
+
throw new MessagingError(
|
|
86
|
+
`Cannot resolve recipient: ${envelope.to} — provide peerHint or ensure agent is in DHT`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const targetPeerId = peerIdFromString(targetPeerIdStr);
|
|
91
|
+
|
|
92
|
+
if (targetMultiaddrs.length > 0) {
|
|
93
|
+
const mas = targetMultiaddrs.map((a) => multiaddr(a));
|
|
94
|
+
await libp2p.peerStore.merge(targetPeerId, { multiaddrs: mas });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
logger.info('Dialing peer for message delivery', {
|
|
98
|
+
peerId: targetPeerIdStr,
|
|
99
|
+
multiaddrs: targetMultiaddrs,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
let stream: any;
|
|
103
|
+
|
|
104
|
+
// Separate relay addrs from direct addrs
|
|
105
|
+
const relayMultiaddrs = targetMultiaddrs.filter((a) => a.includes('/p2p-circuit/'));
|
|
106
|
+
const directMultiaddrs = targetMultiaddrs.filter((a) => !a.includes('/p2p-circuit/'));
|
|
107
|
+
|
|
108
|
+
// Try direct addresses first
|
|
109
|
+
for (const addr of directMultiaddrs) {
|
|
110
|
+
try {
|
|
111
|
+
stream = await libp2p.dialProtocol(multiaddr(addr), PROTOCOL_PREFIX);
|
|
112
|
+
logger.info('Direct dial succeeded', { addr });
|
|
113
|
+
break;
|
|
114
|
+
} catch {
|
|
115
|
+
// try next
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Fall back to relay addresses (dial the full circuit multiaddr directly)
|
|
120
|
+
if (!stream) {
|
|
121
|
+
const allRelayAddrs = [
|
|
122
|
+
...relayMultiaddrs,
|
|
123
|
+
...(relayPeers ?? []).map((r) => buildCircuitRelayAddr(r, targetPeerIdStr!)),
|
|
124
|
+
];
|
|
125
|
+
let lastError: unknown;
|
|
126
|
+
for (const addr of allRelayAddrs) {
|
|
127
|
+
try {
|
|
128
|
+
stream = await libp2p.dialProtocol(multiaddr(addr), PROTOCOL_PREFIX);
|
|
129
|
+
logger.info('Relay dial succeeded', { addr });
|
|
130
|
+
break;
|
|
131
|
+
} catch (relayErr) {
|
|
132
|
+
logger.warn('Relay dial failed', { addr, error: (relayErr as Error).message });
|
|
133
|
+
lastError = relayErr;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (!stream) throw lastError ?? new MessagingError('All dial attempts failed');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const encoded = encodeMessage(envelope);
|
|
140
|
+
await stream.sink(
|
|
141
|
+
(async function* () {
|
|
142
|
+
yield encoded;
|
|
143
|
+
})()
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
logger.info('Message sent over libp2p stream', {
|
|
147
|
+
id: envelope.id,
|
|
148
|
+
from: envelope.from,
|
|
149
|
+
to: envelope.to,
|
|
150
|
+
protocol: envelope.protocol,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// If this is a request, wait for response
|
|
154
|
+
if (envelope.type === 'request') {
|
|
155
|
+
logger.debug('Waiting for response to request', { id: envelope.id });
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
// Add timeout to prevent infinite blocking
|
|
159
|
+
const RESPONSE_TIMEOUT = 30000; // 30 seconds
|
|
160
|
+
|
|
161
|
+
const responsePromise = (async () => {
|
|
162
|
+
const responseChunks: Uint8Array[] = [];
|
|
163
|
+
for await (const chunk of stream.source) {
|
|
164
|
+
responseChunks.push(chunk.subarray());
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (responseChunks.length > 0) {
|
|
168
|
+
const responseData = new Uint8Array(
|
|
169
|
+
responseChunks.reduce((acc, chunk) => acc + chunk.length, 0)
|
|
170
|
+
);
|
|
171
|
+
let offset = 0;
|
|
172
|
+
for (const chunk of responseChunks) {
|
|
173
|
+
responseData.set(chunk, offset);
|
|
174
|
+
offset += chunk.length;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const responseEnvelope = decodeMessage(responseData);
|
|
178
|
+
logger.info('Received response', {
|
|
179
|
+
id: responseEnvelope.id,
|
|
180
|
+
replyTo: responseEnvelope.replyTo,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
return responseEnvelope;
|
|
184
|
+
} else {
|
|
185
|
+
logger.warn('No response received for request', { id: envelope.id });
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
})();
|
|
189
|
+
|
|
190
|
+
const timeoutPromise = new Promise<undefined>((resolve) => {
|
|
191
|
+
setTimeout(() => {
|
|
192
|
+
logger.warn('Response timeout', { id: envelope.id, timeout: RESPONSE_TIMEOUT });
|
|
193
|
+
resolve(undefined);
|
|
194
|
+
}, RESPONSE_TIMEOUT);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const response = await Promise.race([responsePromise, timeoutPromise]);
|
|
198
|
+
return response;
|
|
199
|
+
} catch (error) {
|
|
200
|
+
logger.warn('Error reading response', { error });
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return undefined;
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if (error instanceof MessagingError) throw error;
|
|
208
|
+
throw new MessagingError('Failed to send message', error);
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
start: async () => {
|
|
213
|
+
await libp2p.handle(PROTOCOL_PREFIX, async ({ stream }) => {
|
|
214
|
+
try {
|
|
215
|
+
await handleIncomingStream(stream, handlers, catchAllHandler, verifyFn);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
logger.error('Error handling incoming stream', error);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
logger.info('Message router started');
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
stop: async () => {
|
|
225
|
+
await libp2p.unhandle(PROTOCOL_PREFIX);
|
|
226
|
+
handlers.clear();
|
|
227
|
+
catchAllHandler = undefined;
|
|
228
|
+
logger.info('Message router stopped');
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function buildCircuitRelayAddr(relayAddr: string, targetPeerId: string): string {
|
|
234
|
+
return `${relayAddr}/p2p-circuit/p2p/${targetPeerId}`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function handleIncomingStream(
|
|
238
|
+
stream: any,
|
|
239
|
+
handlers: Map<string, MessageHandler>,
|
|
240
|
+
catchAllHandler: MessageHandler | undefined,
|
|
241
|
+
_verifyFn: (signature: Uint8Array, data: Uint8Array) => Promise<boolean>
|
|
242
|
+
): Promise<void> {
|
|
243
|
+
try {
|
|
244
|
+
const chunks: Uint8Array[] = [];
|
|
245
|
+
for await (const chunk of stream.source) {
|
|
246
|
+
chunks.push(chunk.subarray());
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const data = new Uint8Array(
|
|
250
|
+
chunks.reduce((acc, chunk) => acc + chunk.length, 0)
|
|
251
|
+
);
|
|
252
|
+
let offset = 0;
|
|
253
|
+
for (const chunk of chunks) {
|
|
254
|
+
data.set(chunk, offset);
|
|
255
|
+
offset += chunk.length;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const envelope = decodeMessage(data);
|
|
259
|
+
|
|
260
|
+
if (!validateEnvelope(envelope)) {
|
|
261
|
+
logger.warn('Received invalid message envelope');
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Skip signature verification for now — both sides use auto-generated libp2p keys
|
|
266
|
+
// which differ from the Ed25519 DID keys. Full verification comes in Phase 2 when
|
|
267
|
+
// DID keys and libp2p keys are unified.
|
|
268
|
+
|
|
269
|
+
logger.info('Received message', {
|
|
270
|
+
id: envelope.id,
|
|
271
|
+
from: envelope.from,
|
|
272
|
+
to: envelope.to,
|
|
273
|
+
protocol: envelope.protocol,
|
|
274
|
+
payload: envelope.payload,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const handler = handlers.get(envelope.protocol);
|
|
278
|
+
let response: MessageEnvelope | void = undefined;
|
|
279
|
+
|
|
280
|
+
if (handler) {
|
|
281
|
+
response = await handler(envelope);
|
|
282
|
+
} else if (catchAllHandler) {
|
|
283
|
+
logger.debug('Using catch-all handler for protocol', { protocol: envelope.protocol });
|
|
284
|
+
response = await catchAllHandler(envelope);
|
|
285
|
+
} else {
|
|
286
|
+
logger.warn('No handler for protocol', { protocol: envelope.protocol });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Send response back if handler returned one
|
|
290
|
+
if (response) {
|
|
291
|
+
const encoded = encodeMessage(response);
|
|
292
|
+
logger.info('Sending response back to sender', {
|
|
293
|
+
responseId: response.id,
|
|
294
|
+
replyTo: response.replyTo,
|
|
295
|
+
size: encoded.length
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
await stream.sink(
|
|
299
|
+
(async function* () {
|
|
300
|
+
yield encoded;
|
|
301
|
+
})()
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
} catch (error) {
|
|
305
|
+
logger.error('Error handling incoming message', error);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Libp2p } from 'libp2p';
|
|
2
|
+
import { createLogger } from '../utils/logger.js';
|
|
3
|
+
import { TransportError } from '../utils/errors.js';
|
|
4
|
+
|
|
5
|
+
const logger = createLogger('connection');
|
|
6
|
+
|
|
7
|
+
export interface ConnectionManager {
|
|
8
|
+
connect: (peerId: string) => Promise<void>;
|
|
9
|
+
disconnect: (peerId: string) => Promise<void>;
|
|
10
|
+
isConnected: (peerId: string) => boolean;
|
|
11
|
+
getConnectedPeers: () => string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create a connection manager for a libp2p node
|
|
16
|
+
*/
|
|
17
|
+
export function createConnectionManager(
|
|
18
|
+
libp2p: Libp2p
|
|
19
|
+
): ConnectionManager {
|
|
20
|
+
return {
|
|
21
|
+
connect: async (peerIdStr: string) => {
|
|
22
|
+
try {
|
|
23
|
+
// Parse peer ID and multiaddr
|
|
24
|
+
const connections = libp2p.getConnections();
|
|
25
|
+
const existing = connections.find(
|
|
26
|
+
(conn) => conn.remotePeer.toString() === peerIdStr
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
if (existing) {
|
|
30
|
+
logger.debug('Already connected to peer', { peerId: peerIdStr });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// In a real implementation, we would dial the peer
|
|
35
|
+
// For now, we rely on libp2p's automatic connection management
|
|
36
|
+
logger.info('Connecting to peer', { peerId: peerIdStr });
|
|
37
|
+
} catch (error) {
|
|
38
|
+
throw new TransportError('Failed to connect to peer', error);
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
disconnect: async (peerIdStr: string) => {
|
|
43
|
+
try {
|
|
44
|
+
const connections = libp2p.getConnections();
|
|
45
|
+
const matching = connections.filter(
|
|
46
|
+
(conn) => conn.remotePeer.toString() === peerIdStr
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
for (const conn of matching) {
|
|
50
|
+
await conn.close();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
logger.info('Disconnected from peer', { peerId: peerIdStr });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
throw new TransportError('Failed to disconnect from peer', error);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
isConnected: (peerIdStr: string) => {
|
|
60
|
+
const connections = libp2p.getConnections();
|
|
61
|
+
return connections.some(
|
|
62
|
+
(conn) => conn.remotePeer.toString() === peerIdStr
|
|
63
|
+
);
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
getConnectedPeers: () => {
|
|
67
|
+
const connections = libp2p.getConnections();
|
|
68
|
+
const peers = new Set<string>();
|
|
69
|
+
|
|
70
|
+
for (const conn of connections) {
|
|
71
|
+
peers.add(conn.remotePeer.toString());
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return Array.from(peers);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { createLibp2p, Libp2p } from 'libp2p';
|
|
2
|
+
import { tcp } from '@libp2p/tcp';
|
|
3
|
+
import { noise } from '@chainsafe/libp2p-noise';
|
|
4
|
+
import { mplex } from '@libp2p/mplex';
|
|
5
|
+
import { kadDHT, passthroughMapper } from '@libp2p/kad-dht';
|
|
6
|
+
import { bootstrap } from '@libp2p/bootstrap';
|
|
7
|
+
import { identify } from '@libp2p/identify';
|
|
8
|
+
import { ping } from '@libp2p/ping';
|
|
9
|
+
import {
|
|
10
|
+
circuitRelayTransport,
|
|
11
|
+
circuitRelayServer,
|
|
12
|
+
} from '@libp2p/circuit-relay-v2';
|
|
13
|
+
import { createLogger } from '../utils/logger.js';
|
|
14
|
+
import { TransportError } from '../utils/errors.js';
|
|
15
|
+
import type { KeyPair } from '../identity/keys.js';
|
|
16
|
+
import type { PrivateKey } from '@libp2p/interface';
|
|
17
|
+
|
|
18
|
+
const logger = createLogger('transport');
|
|
19
|
+
|
|
20
|
+
export interface TransportConfig {
|
|
21
|
+
keyPair?: KeyPair;
|
|
22
|
+
listenAddresses?: string[];
|
|
23
|
+
bootstrapPeers?: string[];
|
|
24
|
+
enableDHT?: boolean;
|
|
25
|
+
/** Run as a relay server so NAT'd agents can receive messages through this node */
|
|
26
|
+
enableRelay?: boolean;
|
|
27
|
+
/** libp2p PrivateKey for persistent PeerID (from @libp2p/crypto) */
|
|
28
|
+
privateKey?: PrivateKey;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ClawiverseNode {
|
|
32
|
+
libp2p: Libp2p;
|
|
33
|
+
start: () => Promise<void>;
|
|
34
|
+
stop: () => Promise<void>;
|
|
35
|
+
getMultiaddrs: () => string[];
|
|
36
|
+
getPeerId: () => string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create a Clawiverse transport node with libp2p
|
|
41
|
+
*/
|
|
42
|
+
export async function createNode(
|
|
43
|
+
config: TransportConfig
|
|
44
|
+
): Promise<ClawiverseNode> {
|
|
45
|
+
try {
|
|
46
|
+
const {
|
|
47
|
+
listenAddresses = ['/ip4/0.0.0.0/tcp/0'],
|
|
48
|
+
bootstrapPeers = [],
|
|
49
|
+
enableDHT = true,
|
|
50
|
+
enableRelay = false,
|
|
51
|
+
privateKey,
|
|
52
|
+
} = config;
|
|
53
|
+
|
|
54
|
+
// Add relay listen addresses so libp2p auto-reserves slots on bootstrap relay nodes
|
|
55
|
+
const relayListenAddrs = bootstrapPeers.map((peer) => `${peer}/p2p-circuit`);
|
|
56
|
+
|
|
57
|
+
const libp2pConfig: any = {
|
|
58
|
+
addresses: {
|
|
59
|
+
listen: [...listenAddresses, ...relayListenAddrs],
|
|
60
|
+
},
|
|
61
|
+
...(privateKey ? { privateKey } : {}),
|
|
62
|
+
transports: [
|
|
63
|
+
tcp(),
|
|
64
|
+
circuitRelayTransport(),
|
|
65
|
+
],
|
|
66
|
+
connectionEncrypters: [noise()],
|
|
67
|
+
streamMuxers: [mplex()],
|
|
68
|
+
services: {
|
|
69
|
+
identify: identify(),
|
|
70
|
+
ping: ping(),
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (enableRelay) {
|
|
75
|
+
libp2pConfig.services.relay = circuitRelayServer();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (enableDHT) {
|
|
79
|
+
libp2pConfig.services.dht = kadDHT({
|
|
80
|
+
clientMode: false,
|
|
81
|
+
peerInfoMapper: passthroughMapper,
|
|
82
|
+
validators: {
|
|
83
|
+
clawiverse: async () => {},
|
|
84
|
+
},
|
|
85
|
+
selectors: {
|
|
86
|
+
clawiverse: () => 0,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (bootstrapPeers.length > 0) {
|
|
92
|
+
libp2pConfig.peerDiscovery = [
|
|
93
|
+
bootstrap({
|
|
94
|
+
list: bootstrapPeers,
|
|
95
|
+
}),
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const libp2p = await createLibp2p(libp2pConfig);
|
|
100
|
+
|
|
101
|
+
logger.info('Libp2p node created');
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
libp2p,
|
|
105
|
+
start: async () => {
|
|
106
|
+
await libp2p.start();
|
|
107
|
+
logger.info('Node started', {
|
|
108
|
+
peerId: libp2p.peerId.toString(),
|
|
109
|
+
addresses: libp2p.getMultiaddrs().map((ma) => ma.toString()),
|
|
110
|
+
relay: enableRelay,
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
stop: async () => {
|
|
114
|
+
await libp2p.stop();
|
|
115
|
+
logger.info('Node stopped');
|
|
116
|
+
},
|
|
117
|
+
getMultiaddrs: () => {
|
|
118
|
+
return libp2p.getMultiaddrs().map((ma) => ma.toString());
|
|
119
|
+
},
|
|
120
|
+
getPeerId: () => {
|
|
121
|
+
return libp2p.peerId.toString();
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
} catch (error) {
|
|
125
|
+
throw new TransportError('Failed to create transport node', error);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Endorsement System
|
|
3
|
+
*
|
|
4
|
+
* Allows agents to endorse each other, building a web of trust
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Level } from 'level';
|
|
8
|
+
import { createLogger } from '../utils/logger.js';
|
|
9
|
+
|
|
10
|
+
const logger = createLogger('endorsement');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Endorsement Record
|
|
14
|
+
*/
|
|
15
|
+
export interface Endorsement {
|
|
16
|
+
from: string; // Endorser DID
|
|
17
|
+
to: string; // Endorsed agent DID
|
|
18
|
+
score: number; // 0-1
|
|
19
|
+
reason: string;
|
|
20
|
+
timestamp: number;
|
|
21
|
+
signature: string; // Signed by endorser
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Sign function type
|
|
26
|
+
*/
|
|
27
|
+
export type SignFunction = (data: Uint8Array) => Promise<Uint8Array>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Verify function type
|
|
31
|
+
*/
|
|
32
|
+
export type VerifyFunction = (signature: Uint8Array, data: Uint8Array, publicKey: Uint8Array) => Promise<boolean>;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Endorsement Manager
|
|
36
|
+
*/
|
|
37
|
+
export class EndorsementManager {
|
|
38
|
+
constructor(
|
|
39
|
+
private db: Level<string, Endorsement>,
|
|
40
|
+
private getPublicKey: (did: string) => Promise<Uint8Array>
|
|
41
|
+
) {}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create an endorsement
|
|
45
|
+
*/
|
|
46
|
+
async endorse(
|
|
47
|
+
fromDid: string,
|
|
48
|
+
toDid: string,
|
|
49
|
+
score: number,
|
|
50
|
+
reason: string,
|
|
51
|
+
signFn: SignFunction
|
|
52
|
+
): Promise<Endorsement> {
|
|
53
|
+
if (score < 0 || score > 1) {
|
|
54
|
+
throw new Error('Score must be between 0 and 1');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const endorsement: Omit<Endorsement, 'signature'> = {
|
|
58
|
+
from: fromDid,
|
|
59
|
+
to: toDid,
|
|
60
|
+
score,
|
|
61
|
+
reason,
|
|
62
|
+
timestamp: Date.now(),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Sign the endorsement
|
|
66
|
+
const data = new TextEncoder().encode(JSON.stringify(endorsement));
|
|
67
|
+
const signatureBytes = await signFn(data);
|
|
68
|
+
const signature = Buffer.from(signatureBytes).toString('hex');
|
|
69
|
+
|
|
70
|
+
const signedEndorsement: Endorsement = {
|
|
71
|
+
...endorsement,
|
|
72
|
+
signature,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
logger.info('Created endorsement', { from: fromDid, to: toDid, score });
|
|
76
|
+
return signedEndorsement;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Verify endorsement signature
|
|
81
|
+
*/
|
|
82
|
+
async verify(endorsement: Endorsement, verifyFn: VerifyFunction): Promise<boolean> {
|
|
83
|
+
try {
|
|
84
|
+
const { signature, ...endorsementWithoutSig } = endorsement;
|
|
85
|
+
const data = new TextEncoder().encode(JSON.stringify(endorsementWithoutSig));
|
|
86
|
+
const signatureBytes = Buffer.from(signature, 'hex');
|
|
87
|
+
const publicKey = await this.getPublicKey(endorsement.from);
|
|
88
|
+
|
|
89
|
+
return await verifyFn(signatureBytes, data, publicKey);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
logger.error('Failed to verify endorsement', { error });
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Publish endorsement to local database
|
|
98
|
+
*/
|
|
99
|
+
async publish(endorsement: Endorsement): Promise<void> {
|
|
100
|
+
const key = `endorsement:${endorsement.to}:${endorsement.from}`;
|
|
101
|
+
await this.db.put(key, endorsement);
|
|
102
|
+
logger.info('Published endorsement', { from: endorsement.from, to: endorsement.to });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get all endorsements for an agent
|
|
107
|
+
*/
|
|
108
|
+
async getEndorsements(agentDid: string): Promise<Endorsement[]> {
|
|
109
|
+
const endorsements: Endorsement[] = [];
|
|
110
|
+
const prefix = `endorsement:${agentDid}:`;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
for await (const [_, value] of this.db.iterator({
|
|
114
|
+
gte: prefix,
|
|
115
|
+
lte: prefix + '\xff',
|
|
116
|
+
})) {
|
|
117
|
+
endorsements.push(value);
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
logger.error('Failed to get endorsements', { agentDid, error });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return endorsements;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get endorsements given by an agent
|
|
128
|
+
*/
|
|
129
|
+
async getEndorsementsBy(fromDid: string): Promise<Endorsement[]> {
|
|
130
|
+
const endorsements: Endorsement[] = [];
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
for await (const [_, value] of this.db.iterator()) {
|
|
134
|
+
if (value.from === fromDid) {
|
|
135
|
+
endorsements.push(value);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} catch (error) {
|
|
139
|
+
logger.error('Failed to get endorsements by agent', { fromDid, error });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return endorsements;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Calculate average endorsement score
|
|
147
|
+
*/
|
|
148
|
+
async getAverageScore(agentDid: string): Promise<number> {
|
|
149
|
+
const endorsements = await this.getEndorsements(agentDid);
|
|
150
|
+
|
|
151
|
+
if (endorsements.length === 0) {
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const totalScore = endorsements.reduce((sum, e) => sum + e.score, 0);
|
|
156
|
+
return totalScore / endorsements.length;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Delete an endorsement
|
|
161
|
+
*/
|
|
162
|
+
async deleteEndorsement(fromDid: string, toDid: string): Promise<void> {
|
|
163
|
+
const key = `endorsement:${toDid}:${fromDid}`;
|
|
164
|
+
await this.db.del(key);
|
|
165
|
+
logger.info('Deleted endorsement', { from: fromDid, to: toDid });
|
|
166
|
+
}
|
|
167
|
+
}
|