@highway1/core 0.1.43 → 0.1.45
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 +68 -40
- package/dist/index.js.map +1 -1
- package/package.json +18 -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 +310 -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 +384 -0
- package/src/transport/connection.ts +77 -0
- package/src/transport/index.ts +2 -0
- package/src/transport/node.ts +152 -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,55 @@
|
|
|
1
|
+
import { sign, verify } from './keys.js';
|
|
2
|
+
import { deriveDID } from './did.js';
|
|
3
|
+
import { IdentityError } from '../utils/errors.js';
|
|
4
|
+
|
|
5
|
+
export interface SignedMessage {
|
|
6
|
+
payload: Uint8Array;
|
|
7
|
+
signature: Uint8Array;
|
|
8
|
+
signer: string; // DID
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Sign a message and return a signed message object
|
|
13
|
+
*/
|
|
14
|
+
export async function signMessage(
|
|
15
|
+
payload: Uint8Array,
|
|
16
|
+
privateKey: Uint8Array,
|
|
17
|
+
publicKey: Uint8Array
|
|
18
|
+
): Promise<SignedMessage> {
|
|
19
|
+
try {
|
|
20
|
+
const signature = await sign(payload, privateKey);
|
|
21
|
+
const signer = deriveDID(publicKey);
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
payload,
|
|
25
|
+
signature,
|
|
26
|
+
signer,
|
|
27
|
+
};
|
|
28
|
+
} catch (error) {
|
|
29
|
+
throw new IdentityError('Failed to sign message', error);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Verify a signed message
|
|
35
|
+
*/
|
|
36
|
+
export async function verifyMessage(
|
|
37
|
+
signedMessage: SignedMessage,
|
|
38
|
+
expectedPublicKey: Uint8Array
|
|
39
|
+
): Promise<boolean> {
|
|
40
|
+
try {
|
|
41
|
+
const expectedDID = deriveDID(expectedPublicKey);
|
|
42
|
+
|
|
43
|
+
if (signedMessage.signer !== expectedDID) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return await verify(
|
|
48
|
+
signedMessage.signature,
|
|
49
|
+
signedMessage.payload,
|
|
50
|
+
expectedPublicKey
|
|
51
|
+
);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
throw new IdentityError('Failed to verify message', error);
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clawiverse Core - Main Export
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Identity
|
|
6
|
+
export * from './identity/keys.js';
|
|
7
|
+
export * from './identity/did.js';
|
|
8
|
+
export * from './identity/signer.js';
|
|
9
|
+
|
|
10
|
+
// Transport
|
|
11
|
+
export * from './transport/node.js';
|
|
12
|
+
|
|
13
|
+
// Discovery
|
|
14
|
+
export * from './discovery/agent-card.js';
|
|
15
|
+
export * from './discovery/agent-card-types.js';
|
|
16
|
+
export * from './discovery/agent-card-schema.js';
|
|
17
|
+
export * from './discovery/agent-card-encoder.js';
|
|
18
|
+
export * from './discovery/dht.js';
|
|
19
|
+
export * from './discovery/search-index.js';
|
|
20
|
+
export * from './discovery/capability-matcher.js';
|
|
21
|
+
export * from './discovery/semantic-search.js';
|
|
22
|
+
|
|
23
|
+
// Messaging
|
|
24
|
+
export * from './messaging/envelope.js';
|
|
25
|
+
export * from './messaging/codec.js';
|
|
26
|
+
export * from './messaging/router.js';
|
|
27
|
+
|
|
28
|
+
// Trust (Phase 2)
|
|
29
|
+
export * from './trust/index.js';
|
|
30
|
+
|
|
31
|
+
// Utils
|
|
32
|
+
export * from './utils/logger.js';
|
|
33
|
+
export * from './utils/errors.js';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { encode, decode } from 'cbor-x';
|
|
2
|
+
import type { MessageEnvelope } from './envelope.js';
|
|
3
|
+
import { MessagingError } from '../utils/errors.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Encode a message envelope to CBOR
|
|
7
|
+
*/
|
|
8
|
+
export function encodeMessage(envelope: MessageEnvelope): Uint8Array {
|
|
9
|
+
try {
|
|
10
|
+
return encode(envelope);
|
|
11
|
+
} catch (error) {
|
|
12
|
+
throw new MessagingError('Failed to encode message', error);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Decode a CBOR message to envelope
|
|
18
|
+
*/
|
|
19
|
+
export function decodeMessage(data: Uint8Array): MessageEnvelope {
|
|
20
|
+
try {
|
|
21
|
+
return decode(data) as MessageEnvelope;
|
|
22
|
+
} catch (error) {
|
|
23
|
+
throw new MessagingError('Failed to decode message', error);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Encode message to JSON (for debugging/logging)
|
|
29
|
+
*/
|
|
30
|
+
export function encodeMessageJSON(envelope: MessageEnvelope): string {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.stringify(envelope, null, 2);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
throw new MessagingError('Failed to encode message to JSON', error);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Decode JSON message to envelope
|
|
40
|
+
*/
|
|
41
|
+
export function decodeMessageJSON(json: string): MessageEnvelope {
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(json) as MessageEnvelope;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
throw new MessagingError('Failed to decode message from JSON', error);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { MessagingError } from '../utils/errors.js';
|
|
2
|
+
|
|
3
|
+
export interface MessageEnvelope {
|
|
4
|
+
id: string;
|
|
5
|
+
from: string; // DID
|
|
6
|
+
to: string; // DID
|
|
7
|
+
type: 'request' | 'response' | 'notification';
|
|
8
|
+
protocol: string;
|
|
9
|
+
payload: unknown;
|
|
10
|
+
timestamp: number;
|
|
11
|
+
signature: string;
|
|
12
|
+
replyTo?: string; // For responses, the ID of the request
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create a message envelope
|
|
17
|
+
*/
|
|
18
|
+
export function createEnvelope(
|
|
19
|
+
from: string,
|
|
20
|
+
to: string,
|
|
21
|
+
type: 'request' | 'response' | 'notification',
|
|
22
|
+
protocol: string,
|
|
23
|
+
payload: unknown,
|
|
24
|
+
replyTo?: string
|
|
25
|
+
): Omit<MessageEnvelope, 'signature'> {
|
|
26
|
+
return {
|
|
27
|
+
id: generateMessageId(),
|
|
28
|
+
from,
|
|
29
|
+
to,
|
|
30
|
+
type,
|
|
31
|
+
protocol,
|
|
32
|
+
payload,
|
|
33
|
+
timestamp: Date.now(),
|
|
34
|
+
replyTo,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Sign a message envelope
|
|
40
|
+
*/
|
|
41
|
+
export async function signEnvelope(
|
|
42
|
+
envelope: Omit<MessageEnvelope, 'signature'>,
|
|
43
|
+
signFn: (data: Uint8Array) => Promise<Uint8Array>
|
|
44
|
+
): Promise<MessageEnvelope> {
|
|
45
|
+
try {
|
|
46
|
+
const envelopeJson = JSON.stringify(envelope);
|
|
47
|
+
const envelopeBytes = new TextEncoder().encode(envelopeJson);
|
|
48
|
+
const signature = await signFn(envelopeBytes);
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
...envelope,
|
|
52
|
+
signature: Buffer.from(signature).toString('hex'),
|
|
53
|
+
};
|
|
54
|
+
} catch (error) {
|
|
55
|
+
throw new MessagingError('Failed to sign envelope', error);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Verify a message envelope signature
|
|
61
|
+
*/
|
|
62
|
+
export async function verifyEnvelope(
|
|
63
|
+
envelope: MessageEnvelope,
|
|
64
|
+
verifyFn: (signature: Uint8Array, data: Uint8Array) => Promise<boolean>
|
|
65
|
+
): Promise<boolean> {
|
|
66
|
+
try {
|
|
67
|
+
const { signature, ...envelopeWithoutSig } = envelope;
|
|
68
|
+
const envelopeJson = JSON.stringify(envelopeWithoutSig);
|
|
69
|
+
const envelopeBytes = new TextEncoder().encode(envelopeJson);
|
|
70
|
+
const signatureBytes = Buffer.from(signature, 'hex');
|
|
71
|
+
|
|
72
|
+
return await verifyFn(signatureBytes, envelopeBytes);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
throw new MessagingError('Failed to verify envelope', error);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Validate message envelope structure
|
|
80
|
+
*/
|
|
81
|
+
export function validateEnvelope(msg: unknown): msg is MessageEnvelope {
|
|
82
|
+
if (typeof msg !== 'object' || msg === null) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const m = msg as Partial<MessageEnvelope>;
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
typeof m.id === 'string' &&
|
|
90
|
+
typeof m.from === 'string' &&
|
|
91
|
+
m.from.startsWith('did:clawiverse:') &&
|
|
92
|
+
typeof m.to === 'string' &&
|
|
93
|
+
m.to.startsWith('did:clawiverse:') &&
|
|
94
|
+
(m.type === 'request' || m.type === 'response' || m.type === 'notification') &&
|
|
95
|
+
typeof m.protocol === 'string' &&
|
|
96
|
+
m.payload !== undefined &&
|
|
97
|
+
typeof m.timestamp === 'number' &&
|
|
98
|
+
typeof m.signature === 'string'
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Generate a unique message ID
|
|
104
|
+
*/
|
|
105
|
+
function generateMessageId(): string {
|
|
106
|
+
return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
|
|
107
|
+
}
|
|
@@ -0,0 +1,384 @@
|
|
|
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, verifyEnvelope } from './envelope.js';
|
|
8
|
+
import { createLogger } from '../utils/logger.js';
|
|
9
|
+
import { MessagingError } from '../utils/errors.js';
|
|
10
|
+
import { extractPublicKey } from '../identity/did.js';
|
|
11
|
+
import { verify } from '../identity/keys.js';
|
|
12
|
+
|
|
13
|
+
const logger = createLogger('router');
|
|
14
|
+
|
|
15
|
+
/** Helper to efficiently concatenate Uint8Array chunks */
|
|
16
|
+
function concatUint8Arrays(arrays: Uint8Array[]): Uint8Array {
|
|
17
|
+
if (arrays.length === 0) return new Uint8Array(0);
|
|
18
|
+
if (arrays.length === 1) return arrays[0];
|
|
19
|
+
|
|
20
|
+
const totalLength = arrays.reduce((acc, arr) => acc + arr.length, 0);
|
|
21
|
+
const result = new Uint8Array(totalLength);
|
|
22
|
+
let offset = 0;
|
|
23
|
+
for (const arr of arrays) {
|
|
24
|
+
result.set(arr, offset);
|
|
25
|
+
offset += arr.length;
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type MessageHandler = (
|
|
31
|
+
envelope: MessageEnvelope
|
|
32
|
+
) => Promise<MessageEnvelope | void>;
|
|
33
|
+
|
|
34
|
+
export interface PeerHint {
|
|
35
|
+
peerId: string;
|
|
36
|
+
multiaddrs: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface MessageRouter {
|
|
40
|
+
registerHandler: (protocol: string, handler: MessageHandler) => void;
|
|
41
|
+
unregisterHandler: (protocol: string) => void;
|
|
42
|
+
registerCatchAllHandler: (handler: MessageHandler) => void;
|
|
43
|
+
sendMessage: (envelope: MessageEnvelope, peerHint?: PeerHint) => Promise<MessageEnvelope | void>;
|
|
44
|
+
start: () => Promise<void>;
|
|
45
|
+
stop: () => Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create a message router for a libp2p node.
|
|
50
|
+
* When dht is provided, sendMessage can resolve DIDs to peer addresses via DHT lookup.
|
|
51
|
+
* When relayPeers is provided, sendMessage will attempt relay fallback if direct dial fails.
|
|
52
|
+
*/
|
|
53
|
+
export function createMessageRouter(
|
|
54
|
+
libp2p: Libp2p,
|
|
55
|
+
verifyFn: (signature: Uint8Array, data: Uint8Array) => Promise<boolean>,
|
|
56
|
+
dht?: DHTOperations,
|
|
57
|
+
relayPeers?: string[]
|
|
58
|
+
): MessageRouter {
|
|
59
|
+
const handlers = new Map<string, MessageHandler>();
|
|
60
|
+
let catchAllHandler: MessageHandler | undefined;
|
|
61
|
+
const PROTOCOL_PREFIX = '/clawiverse/msg/1.0.0';
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
registerHandler: (protocol: string, handler: MessageHandler) => {
|
|
65
|
+
handlers.set(protocol, handler);
|
|
66
|
+
logger.info('Registered message handler', { protocol });
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
unregisterHandler: (protocol: string) => {
|
|
70
|
+
handlers.delete(protocol);
|
|
71
|
+
logger.info('Unregistered message handler', { protocol });
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
registerCatchAllHandler: (handler: MessageHandler) => {
|
|
75
|
+
catchAllHandler = handler;
|
|
76
|
+
logger.info('Registered catch-all message handler');
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
sendMessage: async (envelope: MessageEnvelope, peerHint?: PeerHint) => {
|
|
80
|
+
try {
|
|
81
|
+
if (!validateEnvelope(envelope)) {
|
|
82
|
+
throw new MessagingError('Invalid message envelope');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Resolve peer: try peerHint first, then DHT
|
|
86
|
+
let targetPeerIdStr: string | undefined;
|
|
87
|
+
let targetMultiaddrs: string[] = [];
|
|
88
|
+
|
|
89
|
+
if (peerHint) {
|
|
90
|
+
targetPeerIdStr = peerHint.peerId;
|
|
91
|
+
targetMultiaddrs = peerHint.multiaddrs;
|
|
92
|
+
logger.info('Using peer hint for direct addressing', { peerId: targetPeerIdStr });
|
|
93
|
+
} else if (dht) {
|
|
94
|
+
const resolved = await dht.resolveDID(envelope.to);
|
|
95
|
+
if (resolved) {
|
|
96
|
+
targetPeerIdStr = resolved.peerId;
|
|
97
|
+
targetMultiaddrs = resolved.multiaddrs;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!targetPeerIdStr) {
|
|
102
|
+
throw new MessagingError(
|
|
103
|
+
`Cannot resolve recipient: ${envelope.to} — provide peerHint or ensure agent is in DHT`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const targetPeerId = peerIdFromString(targetPeerIdStr);
|
|
108
|
+
|
|
109
|
+
if (targetMultiaddrs.length > 0) {
|
|
110
|
+
const mas = targetMultiaddrs.map((a) => multiaddr(a));
|
|
111
|
+
await libp2p.peerStore.merge(targetPeerId, { multiaddrs: mas });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
logger.info('Dialing peer for message delivery', {
|
|
115
|
+
peerId: targetPeerIdStr,
|
|
116
|
+
multiaddrs: targetMultiaddrs,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
let stream: any;
|
|
120
|
+
const DIAL_TIMEOUT = 3000; // 3 seconds per attempt
|
|
121
|
+
|
|
122
|
+
// Separate relay addrs from direct addrs
|
|
123
|
+
const relayMultiaddrs = targetMultiaddrs.filter((a) => a.includes('/p2p-circuit/'));
|
|
124
|
+
const directMultiaddrs = targetMultiaddrs.filter((a) => !a.includes('/p2p-circuit/'));
|
|
125
|
+
|
|
126
|
+
// Try direct addresses in parallel
|
|
127
|
+
if (directMultiaddrs.length > 0) {
|
|
128
|
+
const directDialPromises = directMultiaddrs.map(async (addr) => {
|
|
129
|
+
try {
|
|
130
|
+
const conn = await libp2p.dial(multiaddr(addr), {
|
|
131
|
+
signal: AbortSignal.timeout(DIAL_TIMEOUT)
|
|
132
|
+
});
|
|
133
|
+
const s = await conn.newStream(PROTOCOL_PREFIX, { runOnLimitedConnection: true });
|
|
134
|
+
logger.info('Direct dial succeeded', { addr });
|
|
135
|
+
return s;
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
stream = await Promise.race(
|
|
142
|
+
directDialPromises.map(p => p.then(s => s || Promise.reject()))
|
|
143
|
+
).catch(() => undefined);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let lastError: unknown;
|
|
147
|
+
|
|
148
|
+
// Fall back to relay: try all relay addresses in parallel
|
|
149
|
+
if (!stream) {
|
|
150
|
+
const allRelayAddrs = [
|
|
151
|
+
...relayMultiaddrs,
|
|
152
|
+
...(relayPeers ?? []).map((r) => buildCircuitRelayAddr(r, targetPeerIdStr!)),
|
|
153
|
+
];
|
|
154
|
+
// Deduplicate
|
|
155
|
+
const uniqueRelayAddrs = [...new Set(allRelayAddrs)];
|
|
156
|
+
|
|
157
|
+
if (uniqueRelayAddrs.length > 0) {
|
|
158
|
+
const relayDialPromises = uniqueRelayAddrs.map(async (addr) => {
|
|
159
|
+
try {
|
|
160
|
+
const conn = await libp2p.dial(multiaddr(addr), {
|
|
161
|
+
signal: AbortSignal.timeout(DIAL_TIMEOUT)
|
|
162
|
+
});
|
|
163
|
+
logger.info('Relay connection established', { addr });
|
|
164
|
+
const s = await conn.newStream(PROTOCOL_PREFIX, { runOnLimitedConnection: true });
|
|
165
|
+
logger.info('Relay stream opened', { addr });
|
|
166
|
+
return s;
|
|
167
|
+
} catch (relayErr) {
|
|
168
|
+
logger.warn('Relay dial failed', { addr, error: (relayErr as Error).message });
|
|
169
|
+
lastError = relayErr;
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
stream = await Promise.race(
|
|
175
|
+
relayDialPromises.map(p => p.then(s => s || Promise.reject()))
|
|
176
|
+
).catch(() => undefined);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Last resort: query DHT for relay-capable peers
|
|
181
|
+
if (!stream && dht && 'queryRelayPeers' in dht) {
|
|
182
|
+
const discoveredRelays = await (dht as any).queryRelayPeers();
|
|
183
|
+
for (const relayAddr of discoveredRelays) {
|
|
184
|
+
const circuitAddr = buildCircuitRelayAddr(relayAddr, targetPeerIdStr!);
|
|
185
|
+
try {
|
|
186
|
+
const conn = await libp2p.dial(multiaddr(circuitAddr));
|
|
187
|
+
stream = await conn.newStream(PROTOCOL_PREFIX, { runOnLimitedConnection: true });
|
|
188
|
+
logger.info('DHT-discovered relay succeeded', { relayAddr });
|
|
189
|
+
break;
|
|
190
|
+
} catch (e) {
|
|
191
|
+
logger.warn('DHT relay failed', { relayAddr, error: (e as Error).message });
|
|
192
|
+
lastError = e;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!stream) {
|
|
198
|
+
throw lastError ?? new MessagingError('All dial attempts failed (including DHT-discovered relays)');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const encoded = encodeMessage(envelope);
|
|
202
|
+
await stream.sink(
|
|
203
|
+
(async function* () {
|
|
204
|
+
yield encoded;
|
|
205
|
+
})()
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
logger.info('Message sent over libp2p stream', {
|
|
209
|
+
id: envelope.id,
|
|
210
|
+
from: envelope.from,
|
|
211
|
+
to: envelope.to,
|
|
212
|
+
protocol: envelope.protocol,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// If this is a request, wait for response
|
|
216
|
+
if (envelope.type === 'request') {
|
|
217
|
+
logger.debug('Waiting for response to request', { id: envelope.id });
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
// Add timeout to prevent infinite blocking
|
|
221
|
+
const RESPONSE_TIMEOUT = 5000; // 5 seconds
|
|
222
|
+
|
|
223
|
+
const responsePromise = (async () => {
|
|
224
|
+
const responseChunks: Uint8Array[] = [];
|
|
225
|
+
for await (const chunk of stream.source) {
|
|
226
|
+
responseChunks.push(chunk.subarray());
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (responseChunks.length > 0) {
|
|
230
|
+
const responseData = concatUint8Arrays(responseChunks);
|
|
231
|
+
const responseEnvelope = decodeMessage(responseData);
|
|
232
|
+
logger.info('Received response', {
|
|
233
|
+
id: responseEnvelope.id,
|
|
234
|
+
replyTo: responseEnvelope.replyTo,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
return responseEnvelope;
|
|
238
|
+
} else {
|
|
239
|
+
logger.warn('No response received for request', { id: envelope.id });
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
})();
|
|
243
|
+
|
|
244
|
+
const timeoutPromise = new Promise<undefined>((resolve) => {
|
|
245
|
+
setTimeout(() => {
|
|
246
|
+
logger.warn('Response timeout', { id: envelope.id, timeout: RESPONSE_TIMEOUT });
|
|
247
|
+
resolve(undefined);
|
|
248
|
+
}, RESPONSE_TIMEOUT);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const response = await Promise.race([responsePromise, timeoutPromise]);
|
|
252
|
+
return response;
|
|
253
|
+
} catch (error) {
|
|
254
|
+
logger.warn('Error reading response', { error });
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return undefined;
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if (error instanceof MessagingError) throw error;
|
|
262
|
+
throw new MessagingError('Failed to send message', error);
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
start: async () => {
|
|
267
|
+
await libp2p.handle(PROTOCOL_PREFIX, async ({ stream }) => {
|
|
268
|
+
try {
|
|
269
|
+
await handleIncomingStream(stream, handlers, catchAllHandler, verifyFn);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
logger.error('Error handling incoming stream', error);
|
|
272
|
+
}
|
|
273
|
+
}, { runOnLimitedConnection: true });
|
|
274
|
+
|
|
275
|
+
logger.info('Message router started');
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
stop: async () => {
|
|
279
|
+
await libp2p.unhandle(PROTOCOL_PREFIX);
|
|
280
|
+
handlers.clear();
|
|
281
|
+
catchAllHandler = undefined;
|
|
282
|
+
logger.info('Message router stopped');
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function buildCircuitRelayAddr(relayAddr: string, targetPeerId: string): string {
|
|
288
|
+
return `${relayAddr}/p2p-circuit/p2p/${targetPeerId}`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function handleIncomingStream(
|
|
292
|
+
stream: any,
|
|
293
|
+
handlers: Map<string, MessageHandler>,
|
|
294
|
+
catchAllHandler: MessageHandler | undefined,
|
|
295
|
+
verifyFn: (signature: Uint8Array, data: Uint8Array) => Promise<boolean>
|
|
296
|
+
): Promise<void> {
|
|
297
|
+
try {
|
|
298
|
+
const chunks: Uint8Array[] = [];
|
|
299
|
+
for await (const chunk of stream.source) {
|
|
300
|
+
chunks.push(chunk.subarray());
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const data = concatUint8Arrays(chunks);
|
|
304
|
+
const envelope = decodeMessage(data);
|
|
305
|
+
|
|
306
|
+
if (!validateEnvelope(envelope)) {
|
|
307
|
+
logger.warn('Received invalid message envelope');
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Verify signature against sender DID embedded public key.
|
|
312
|
+
const isValidSignature = await verifyEnvelope(envelope, async (signature, data) => {
|
|
313
|
+
const senderPublicKey = extractPublicKey(envelope.from);
|
|
314
|
+
return verify(signature, data, senderPublicKey);
|
|
315
|
+
});
|
|
316
|
+
if (!isValidSignature) {
|
|
317
|
+
logger.warn('Received message with invalid signature', {
|
|
318
|
+
id: envelope.id,
|
|
319
|
+
from: envelope.from,
|
|
320
|
+
});
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Optional caller-supplied verification hook (e.g. policy checks).
|
|
325
|
+
try {
|
|
326
|
+
const { signature, ...envelopeWithoutSig } = envelope;
|
|
327
|
+
const dataBytes = new TextEncoder().encode(JSON.stringify(envelopeWithoutSig));
|
|
328
|
+
const signatureBytes = Buffer.from(signature, 'hex');
|
|
329
|
+
const hookValid = await verifyFn(signatureBytes, dataBytes);
|
|
330
|
+
if (!hookValid) {
|
|
331
|
+
logger.warn('Message rejected by custom verifier', {
|
|
332
|
+
id: envelope.id,
|
|
333
|
+
from: envelope.from,
|
|
334
|
+
});
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
} catch (error) {
|
|
338
|
+
logger.warn('Custom verification hook failed', {
|
|
339
|
+
id: envelope.id,
|
|
340
|
+
from: envelope.from,
|
|
341
|
+
error: (error as Error).message,
|
|
342
|
+
});
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
logger.info('Received message', {
|
|
347
|
+
id: envelope.id,
|
|
348
|
+
from: envelope.from,
|
|
349
|
+
to: envelope.to,
|
|
350
|
+
protocol: envelope.protocol,
|
|
351
|
+
payload: envelope.payload,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const handler = handlers.get(envelope.protocol);
|
|
355
|
+
let response: MessageEnvelope | void = undefined;
|
|
356
|
+
|
|
357
|
+
if (handler) {
|
|
358
|
+
response = await handler(envelope);
|
|
359
|
+
} else if (catchAllHandler) {
|
|
360
|
+
logger.debug('Using catch-all handler for protocol', { protocol: envelope.protocol });
|
|
361
|
+
response = await catchAllHandler(envelope);
|
|
362
|
+
} else {
|
|
363
|
+
logger.warn('No handler for protocol', { protocol: envelope.protocol });
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Send response back if handler returned one
|
|
367
|
+
if (response) {
|
|
368
|
+
const encoded = encodeMessage(response);
|
|
369
|
+
logger.info('Sending response back to sender', {
|
|
370
|
+
responseId: response.id,
|
|
371
|
+
replyTo: response.replyTo,
|
|
372
|
+
size: encoded.length
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
await stream.sink(
|
|
376
|
+
(async function* () {
|
|
377
|
+
yield encoded;
|
|
378
|
+
})()
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
} catch (error) {
|
|
382
|
+
logger.error('Error handling incoming message', error);
|
|
383
|
+
}
|
|
384
|
+
}
|