@highway1/core 0.1.48 → 0.1.53
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.d.ts +190 -46
- package/dist/index.js +517 -3394
- package/dist/index.js.map +1 -1
- package/package.json +4 -16
- package/src/discovery/relay-index.ts +98 -0
- package/src/index.ts +4 -3
- package/src/messaging/router.ts +121 -327
- package/src/transport/relay-client.ts +437 -0
- package/src/transport/relay-types.ts +196 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@highway1/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.53",
|
|
4
4
|
"description": "Core protocol implementation for Clawiverse",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -18,34 +18,22 @@
|
|
|
18
18
|
"clean": "rm -rf dist"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@chainsafe/libp2p-noise": "^16.0.0",
|
|
22
|
-
"@libp2p/bootstrap": "^11.0.0",
|
|
23
|
-
"@libp2p/mdns": "^12.0.0",
|
|
24
|
-
"@libp2p/circuit-relay-v2": "^3.1.0",
|
|
25
|
-
"@libp2p/identify": "^3.0.0",
|
|
26
|
-
"@libp2p/kad-dht": "^13.0.0",
|
|
27
|
-
"@libp2p/mplex": "^11.0.0",
|
|
28
|
-
"@libp2p/peer-id": "^5.0.0",
|
|
29
|
-
"@libp2p/ping": "^2.0.0",
|
|
30
|
-
"@libp2p/tcp": "^10.1.0",
|
|
31
|
-
"@multiformats/multiaddr": "^12.4.0",
|
|
32
21
|
"@noble/ed25519": "^2.1.0",
|
|
33
22
|
"@noble/hashes": "^1.8.0",
|
|
23
|
+
"@noble/ciphers": "^0.5.0",
|
|
34
24
|
"ajv": "^8.17.1",
|
|
35
25
|
"cbor-x": "^1.6.0",
|
|
36
26
|
"fuse.js": "^7.0.0",
|
|
37
|
-
"it-pipe": "^3.0.1",
|
|
38
27
|
"level": "^8.0.0",
|
|
39
|
-
"libp2p": "^2.10.0",
|
|
40
28
|
"lunr": "^2.3.9",
|
|
41
29
|
"multiformats": "^13.3.1",
|
|
42
|
-
"
|
|
30
|
+
"ws": "^8.18.0"
|
|
43
31
|
},
|
|
44
32
|
"devDependencies": {
|
|
45
|
-
"@libp2p/interface": "^2.11.0",
|
|
46
33
|
"@types/level": "^6.0.0",
|
|
47
34
|
"@types/lunr": "^2.3.7",
|
|
48
35
|
"@types/node": "^22.10.2",
|
|
36
|
+
"@types/ws": "^8.5.10",
|
|
49
37
|
"tsup": "^8.3.5",
|
|
50
38
|
"typescript": "^5.7.2",
|
|
51
39
|
"vitest": "^2.1.8"
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CVP-0011: Relay-backed discovery operations
|
|
3
|
+
* Implements the same DHTOperations interface shape but backed by relay queries
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { AgentCard } from './agent-card-types.js';
|
|
7
|
+
import type { SemanticQuery } from './search-index.js';
|
|
8
|
+
import type { RelayClient } from '../transport/relay-client.js';
|
|
9
|
+
import { createLogger } from '../utils/logger.js';
|
|
10
|
+
import { DiscoveryError } from '../utils/errors.js';
|
|
11
|
+
|
|
12
|
+
const logger = createLogger('relay-index');
|
|
13
|
+
|
|
14
|
+
export interface RelayIndexOperations {
|
|
15
|
+
publishAgentCard: (card: AgentCard) => Promise<void>;
|
|
16
|
+
queryAgentCard: (did: string) => Promise<AgentCard | null>;
|
|
17
|
+
queryByCapability: (capability: string) => Promise<AgentCard[]>;
|
|
18
|
+
searchSemantic: (query: SemanticQuery) => Promise<AgentCard[]>;
|
|
19
|
+
resolveDID: (did: string) => Promise<{ relayUrl: string } | null>;
|
|
20
|
+
queryRelayPeers: () => Promise<string[]>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create relay-backed discovery operations
|
|
25
|
+
*/
|
|
26
|
+
export function createRelayIndexOperations(client: RelayClient): RelayIndexOperations {
|
|
27
|
+
return {
|
|
28
|
+
publishAgentCard: async (card: AgentCard) => {
|
|
29
|
+
// No-op: Agent Card is published via HELLO message when connecting to relay
|
|
30
|
+
logger.debug('Agent Card published via HELLO', { did: card.did });
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
queryAgentCard: async (did: string) => {
|
|
34
|
+
try {
|
|
35
|
+
const card = await client.fetchCard(did);
|
|
36
|
+
if (card) {
|
|
37
|
+
logger.debug('Found Agent Card via relay', { did });
|
|
38
|
+
} else {
|
|
39
|
+
logger.debug('Agent Card not found via relay', { did });
|
|
40
|
+
}
|
|
41
|
+
return card;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
logger.warn('Failed to query Agent Card', { did, error });
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
queryByCapability: async (capability: string) => {
|
|
49
|
+
try {
|
|
50
|
+
const results = await client.discover(capability);
|
|
51
|
+
const cards = results.map((r) => r.card);
|
|
52
|
+
logger.debug('Query by capability', { capability, count: cards.length });
|
|
53
|
+
return cards;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
throw new DiscoveryError('Failed to query by capability', error);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
searchSemantic: async (query: SemanticQuery) => {
|
|
60
|
+
try {
|
|
61
|
+
const queryText = query.text || query.capability || '';
|
|
62
|
+
const minTrust = query.filters?.minTrustScore;
|
|
63
|
+
const limit = query.limit || 10;
|
|
64
|
+
|
|
65
|
+
const results = await client.discover(queryText, minTrust, limit);
|
|
66
|
+
const cards = results.map((r) => r.card);
|
|
67
|
+
|
|
68
|
+
logger.debug('Semantic search', { query: queryText, count: cards.length });
|
|
69
|
+
return cards;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
throw new DiscoveryError('Failed to perform semantic search', error);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
resolveDID: async (did: string) => {
|
|
76
|
+
try {
|
|
77
|
+
// In relay architecture, we don't need peer resolution
|
|
78
|
+
// The relay handles routing, so we just return the relay URL
|
|
79
|
+
const relays = client.getConnectedRelays();
|
|
80
|
+
if (relays.length === 0) {
|
|
81
|
+
logger.debug('DID resolution failed: no connected relays', { did });
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
logger.debug('Resolved DID to relay', { did, relay: relays[0] });
|
|
86
|
+
return { relayUrl: relays[0] };
|
|
87
|
+
} catch (error) {
|
|
88
|
+
logger.warn('Failed to resolve DID', { did, error });
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
queryRelayPeers: async () => {
|
|
94
|
+
// Return connected relay URLs
|
|
95
|
+
return client.getConnectedRelays();
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -7,15 +7,16 @@ export * from './identity/keys.js';
|
|
|
7
7
|
export * from './identity/did.js';
|
|
8
8
|
export * from './identity/signer.js';
|
|
9
9
|
|
|
10
|
-
// Transport
|
|
11
|
-
export * from './transport/
|
|
10
|
+
// Transport (CVP-0011: relay-based)
|
|
11
|
+
export * from './transport/relay-client.js';
|
|
12
|
+
export * from './transport/relay-types.js';
|
|
12
13
|
|
|
13
14
|
// Discovery
|
|
14
15
|
export * from './discovery/agent-card.js';
|
|
15
16
|
export * from './discovery/agent-card-types.js';
|
|
16
17
|
export * from './discovery/agent-card-schema.js';
|
|
17
18
|
export * from './discovery/agent-card-encoder.js';
|
|
18
|
-
export * from './discovery/
|
|
19
|
+
export * from './discovery/relay-index.js';
|
|
19
20
|
export * from './discovery/search-index.js';
|
|
20
21
|
export * from './discovery/capability-matcher.js';
|
|
21
22
|
export * from './discovery/semantic-search.js';
|
package/src/messaging/router.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import type { Libp2p } from 'libp2p';
|
|
2
|
-
import { peerIdFromString } from '@libp2p/peer-id';
|
|
3
|
-
import { multiaddr } from '@multiformats/multiaddr';
|
|
4
1
|
import type { MessageEnvelope } from './envelope.js';
|
|
5
|
-
import type {
|
|
2
|
+
import type { RelayClient } from '../transport/relay-client.js';
|
|
6
3
|
import { encodeMessage, decodeMessage } from './codec.js';
|
|
7
4
|
import { validateEnvelope, verifyEnvelope } from './envelope.js';
|
|
8
5
|
import { createLogger } from '../utils/logger.js';
|
|
@@ -12,53 +9,34 @@ import { verify } from '../identity/keys.js';
|
|
|
12
9
|
|
|
13
10
|
const logger = createLogger('router');
|
|
14
11
|
|
|
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
12
|
export type MessageHandler = (
|
|
31
13
|
envelope: MessageEnvelope
|
|
32
14
|
) => Promise<MessageEnvelope | void>;
|
|
33
15
|
|
|
34
|
-
export interface PeerHint {
|
|
35
|
-
peerId: string;
|
|
36
|
-
multiaddrs: string[];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
16
|
export interface MessageRouter {
|
|
40
17
|
registerHandler: (protocol: string, handler: MessageHandler) => void;
|
|
41
18
|
unregisterHandler: (protocol: string) => void;
|
|
42
19
|
registerCatchAllHandler: (handler: MessageHandler) => void;
|
|
43
|
-
sendMessage: (envelope: MessageEnvelope
|
|
20
|
+
sendMessage: (envelope: MessageEnvelope) => Promise<MessageEnvelope | void>;
|
|
44
21
|
start: () => Promise<void>;
|
|
45
22
|
stop: () => Promise<void>;
|
|
46
23
|
}
|
|
47
24
|
|
|
48
25
|
/**
|
|
49
|
-
* Create a message router
|
|
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.
|
|
26
|
+
* Create a message router using relay client (CVP-0011)
|
|
52
27
|
*/
|
|
53
28
|
export function createMessageRouter(
|
|
54
|
-
|
|
55
|
-
verifyFn: (signature: Uint8Array, data: Uint8Array) => Promise<boolean
|
|
56
|
-
dht?: DHTOperations,
|
|
57
|
-
relayPeers?: string[]
|
|
29
|
+
relayClient: RelayClient,
|
|
30
|
+
verifyFn: (signature: Uint8Array, data: Uint8Array) => Promise<boolean>
|
|
58
31
|
): MessageRouter {
|
|
59
32
|
const handlers = new Map<string, MessageHandler>();
|
|
60
33
|
let catchAllHandler: MessageHandler | undefined;
|
|
61
|
-
|
|
34
|
+
|
|
35
|
+
// Map to track pending requests waiting for responses
|
|
36
|
+
const pendingRequests = new Map<string, {
|
|
37
|
+
resolve: (envelope: MessageEnvelope | undefined) => void;
|
|
38
|
+
timeout: NodeJS.Timeout;
|
|
39
|
+
}>();
|
|
62
40
|
|
|
63
41
|
return {
|
|
64
42
|
registerHandler: (protocol: string, handler: MessageHandler) => {
|
|
@@ -76,160 +54,16 @@ export function createMessageRouter(
|
|
|
76
54
|
logger.info('Registered catch-all message handler');
|
|
77
55
|
},
|
|
78
56
|
|
|
79
|
-
sendMessage: async (envelope: MessageEnvelope
|
|
57
|
+
sendMessage: async (envelope: MessageEnvelope) => {
|
|
80
58
|
try {
|
|
81
59
|
if (!validateEnvelope(envelope)) {
|
|
82
60
|
throw new MessagingError('Invalid message envelope');
|
|
83
61
|
}
|
|
84
62
|
|
|
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 { conn, stream: s };
|
|
136
|
-
} catch {
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
const winner = await Promise.race(
|
|
142
|
-
directDialPromises.map(p => p.then(r => r || Promise.reject()))
|
|
143
|
-
).catch(() => undefined);
|
|
144
|
-
|
|
145
|
-
if (winner) {
|
|
146
|
-
stream = winner.stream;
|
|
147
|
-
// Close connections whose streams were not selected
|
|
148
|
-
Promise.allSettled(directDialPromises).then((results) => {
|
|
149
|
-
for (const result of results) {
|
|
150
|
-
if (result.status === 'fulfilled' && result.value && result.value.stream !== stream) {
|
|
151
|
-
result.value.conn.close().catch(() => {});
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
let lastError: unknown;
|
|
159
|
-
|
|
160
|
-
// Fall back to relay: try all relay addresses in parallel
|
|
161
|
-
if (!stream) {
|
|
162
|
-
const allRelayAddrs = [
|
|
163
|
-
...relayMultiaddrs,
|
|
164
|
-
...(relayPeers ?? []).map((r) => buildCircuitRelayAddr(r, targetPeerIdStr!)),
|
|
165
|
-
];
|
|
166
|
-
// Deduplicate
|
|
167
|
-
const uniqueRelayAddrs = [...new Set(allRelayAddrs)];
|
|
168
|
-
|
|
169
|
-
if (uniqueRelayAddrs.length > 0) {
|
|
170
|
-
const relayDialPromises = uniqueRelayAddrs.map(async (addr) => {
|
|
171
|
-
try {
|
|
172
|
-
const conn = await libp2p.dial(multiaddr(addr), {
|
|
173
|
-
signal: AbortSignal.timeout(DIAL_TIMEOUT)
|
|
174
|
-
});
|
|
175
|
-
logger.info('Relay connection established', { addr });
|
|
176
|
-
const s = await conn.newStream(PROTOCOL_PREFIX, { runOnLimitedConnection: true });
|
|
177
|
-
logger.info('Relay stream opened', { addr });
|
|
178
|
-
return { conn, stream: s };
|
|
179
|
-
} catch (relayErr) {
|
|
180
|
-
logger.warn('Relay dial failed', { addr, error: (relayErr as Error).message });
|
|
181
|
-
lastError = relayErr;
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
const winner = await Promise.race(
|
|
187
|
-
relayDialPromises.map(p => p.then(r => r || Promise.reject()))
|
|
188
|
-
).catch(() => undefined);
|
|
189
|
-
|
|
190
|
-
if (winner) {
|
|
191
|
-
stream = winner.stream;
|
|
192
|
-
// Close connections whose streams were not selected
|
|
193
|
-
Promise.allSettled(relayDialPromises).then((results) => {
|
|
194
|
-
for (const result of results) {
|
|
195
|
-
if (result.status === 'fulfilled' && result.value && result.value.stream !== stream) {
|
|
196
|
-
result.value.conn.close().catch(() => {});
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Last resort: query DHT for relay-capable peers
|
|
205
|
-
if (!stream && dht && 'queryRelayPeers' in dht) {
|
|
206
|
-
const discoveredRelays = await (dht as any).queryRelayPeers();
|
|
207
|
-
for (const relayAddr of discoveredRelays) {
|
|
208
|
-
const circuitAddr = buildCircuitRelayAddr(relayAddr, targetPeerIdStr!);
|
|
209
|
-
try {
|
|
210
|
-
const conn = await libp2p.dial(multiaddr(circuitAddr));
|
|
211
|
-
stream = await conn.newStream(PROTOCOL_PREFIX, { runOnLimitedConnection: true });
|
|
212
|
-
logger.info('DHT-discovered relay succeeded', { relayAddr });
|
|
213
|
-
break;
|
|
214
|
-
} catch (e) {
|
|
215
|
-
logger.warn('DHT relay failed', { relayAddr, error: (e as Error).message });
|
|
216
|
-
lastError = e;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (!stream) {
|
|
222
|
-
throw lastError ?? new MessagingError('All dial attempts failed (including DHT-discovered relays)');
|
|
223
|
-
}
|
|
224
|
-
|
|
225
63
|
const encoded = encodeMessage(envelope);
|
|
226
|
-
await
|
|
227
|
-
(async function* () {
|
|
228
|
-
yield encoded;
|
|
229
|
-
})()
|
|
230
|
-
);
|
|
64
|
+
await relayClient.sendEnvelope(envelope.to, encoded);
|
|
231
65
|
|
|
232
|
-
logger.info('Message sent
|
|
66
|
+
logger.info('Message sent via relay', {
|
|
233
67
|
id: envelope.id,
|
|
234
68
|
from: envelope.from,
|
|
235
69
|
to: envelope.to,
|
|
@@ -240,44 +74,17 @@ export function createMessageRouter(
|
|
|
240
74
|
if (envelope.type === 'request') {
|
|
241
75
|
logger.debug('Waiting for response to request', { id: envelope.id });
|
|
242
76
|
|
|
243
|
-
|
|
244
|
-
// Add timeout to prevent infinite blocking
|
|
245
|
-
const RESPONSE_TIMEOUT = 30000; // 30 seconds (CVP-0010 §4.2)
|
|
77
|
+
const RESPONSE_TIMEOUT = 30000; // 30 seconds (CVP-0010 §4.2)
|
|
246
78
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
79
|
+
return new Promise<MessageEnvelope | undefined>((resolve) => {
|
|
80
|
+
const timeout = setTimeout(() => {
|
|
81
|
+
pendingRequests.delete(envelope.id);
|
|
82
|
+
logger.warn('Response timeout', { id: envelope.id, timeout: RESPONSE_TIMEOUT });
|
|
83
|
+
resolve(undefined);
|
|
84
|
+
}, RESPONSE_TIMEOUT);
|
|
252
85
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const responseEnvelope = decodeMessage(responseData);
|
|
256
|
-
logger.info('Received response', {
|
|
257
|
-
id: responseEnvelope.id,
|
|
258
|
-
replyTo: responseEnvelope.replyTo,
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
return responseEnvelope;
|
|
262
|
-
} else {
|
|
263
|
-
logger.warn('No response received for request', { id: envelope.id });
|
|
264
|
-
return undefined;
|
|
265
|
-
}
|
|
266
|
-
})();
|
|
267
|
-
|
|
268
|
-
const timeoutPromise = new Promise<undefined>((resolve) => {
|
|
269
|
-
setTimeout(() => {
|
|
270
|
-
logger.warn('Response timeout', { id: envelope.id, timeout: RESPONSE_TIMEOUT });
|
|
271
|
-
resolve(undefined);
|
|
272
|
-
}, RESPONSE_TIMEOUT);
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
const response = await Promise.race([responsePromise, timeoutPromise]);
|
|
276
|
-
return response;
|
|
277
|
-
} catch (error) {
|
|
278
|
-
logger.warn('Error reading response', { error });
|
|
279
|
-
return undefined;
|
|
280
|
-
}
|
|
86
|
+
pendingRequests.set(envelope.id, { resolve, timeout });
|
|
87
|
+
});
|
|
281
88
|
}
|
|
282
89
|
|
|
283
90
|
return undefined;
|
|
@@ -288,128 +95,115 @@ export function createMessageRouter(
|
|
|
288
95
|
},
|
|
289
96
|
|
|
290
97
|
start: async () => {
|
|
291
|
-
|
|
98
|
+
// Register handler for incoming DELIVER messages
|
|
99
|
+
relayClient.onDeliver(async (deliverMsg) => {
|
|
292
100
|
try {
|
|
293
|
-
|
|
101
|
+
const envelope = decodeMessage(deliverMsg.envelope);
|
|
102
|
+
|
|
103
|
+
if (!validateEnvelope(envelope)) {
|
|
104
|
+
logger.warn('Received invalid message envelope');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Verify signature against sender DID embedded public key
|
|
109
|
+
const isValidSignature = await verifyEnvelope(envelope, async (signature, data) => {
|
|
110
|
+
const senderPublicKey = extractPublicKey(envelope.from);
|
|
111
|
+
return verify(signature, data, senderPublicKey);
|
|
112
|
+
});
|
|
113
|
+
if (!isValidSignature) {
|
|
114
|
+
logger.warn('Received message with invalid signature', {
|
|
115
|
+
id: envelope.id,
|
|
116
|
+
from: envelope.from,
|
|
117
|
+
});
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Optional caller-supplied verification hook (e.g. policy checks)
|
|
122
|
+
try {
|
|
123
|
+
const { signature, ...envelopeWithoutSig } = envelope;
|
|
124
|
+
const dataBytes = new TextEncoder().encode(JSON.stringify(envelopeWithoutSig));
|
|
125
|
+
const signatureBytes = Buffer.from(signature, 'hex');
|
|
126
|
+
const hookValid = await verifyFn(signatureBytes, dataBytes);
|
|
127
|
+
if (!hookValid) {
|
|
128
|
+
logger.warn('Message rejected by custom verifier', {
|
|
129
|
+
id: envelope.id,
|
|
130
|
+
from: envelope.from,
|
|
131
|
+
});
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
} catch (error) {
|
|
135
|
+
logger.warn('Custom verification hook failed', {
|
|
136
|
+
id: envelope.id,
|
|
137
|
+
from: envelope.from,
|
|
138
|
+
error: (error as Error).message,
|
|
139
|
+
});
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
logger.info('Received message', {
|
|
144
|
+
id: envelope.id,
|
|
145
|
+
from: envelope.from,
|
|
146
|
+
to: envelope.to,
|
|
147
|
+
protocol: envelope.protocol,
|
|
148
|
+
type: envelope.type,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Check if this is a response to a pending request
|
|
152
|
+
if (envelope.type === 'response' && envelope.replyTo) {
|
|
153
|
+
const pending = pendingRequests.get(envelope.replyTo);
|
|
154
|
+
if (pending) {
|
|
155
|
+
clearTimeout(pending.timeout);
|
|
156
|
+
pendingRequests.delete(envelope.replyTo);
|
|
157
|
+
pending.resolve(envelope);
|
|
158
|
+
logger.info('Matched response to pending request', {
|
|
159
|
+
requestId: envelope.replyTo,
|
|
160
|
+
responseId: envelope.id,
|
|
161
|
+
});
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Dispatch to handler
|
|
167
|
+
const handler = handlers.get(envelope.protocol);
|
|
168
|
+
let response: MessageEnvelope | void = undefined;
|
|
169
|
+
|
|
170
|
+
if (handler) {
|
|
171
|
+
response = await handler(envelope);
|
|
172
|
+
} else if (catchAllHandler) {
|
|
173
|
+
logger.debug('Using catch-all handler for protocol', { protocol: envelope.protocol });
|
|
174
|
+
response = await catchAllHandler(envelope);
|
|
175
|
+
} else {
|
|
176
|
+
logger.warn('No handler for protocol', { protocol: envelope.protocol });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Send response back if handler returned one
|
|
180
|
+
if (response) {
|
|
181
|
+
const encoded = encodeMessage(response);
|
|
182
|
+
await relayClient.sendEnvelope(response.to, encoded);
|
|
183
|
+
logger.info('Sent response back to sender', {
|
|
184
|
+
responseId: response.id,
|
|
185
|
+
replyTo: response.replyTo,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
294
188
|
} catch (error) {
|
|
295
|
-
logger.error('Error handling incoming
|
|
189
|
+
logger.error('Error handling incoming message', error);
|
|
296
190
|
}
|
|
297
|
-
}
|
|
191
|
+
});
|
|
298
192
|
|
|
299
193
|
logger.info('Message router started');
|
|
300
194
|
},
|
|
301
195
|
|
|
302
196
|
stop: async () => {
|
|
303
|
-
|
|
197
|
+
// Clear all pending requests
|
|
198
|
+
for (const [, pending] of pendingRequests.entries()) {
|
|
199
|
+
clearTimeout(pending.timeout);
|
|
200
|
+
pending.resolve(undefined);
|
|
201
|
+
}
|
|
202
|
+
pendingRequests.clear();
|
|
203
|
+
|
|
304
204
|
handlers.clear();
|
|
305
205
|
catchAllHandler = undefined;
|
|
306
206
|
logger.info('Message router stopped');
|
|
307
207
|
},
|
|
308
208
|
};
|
|
309
209
|
}
|
|
310
|
-
|
|
311
|
-
function buildCircuitRelayAddr(relayAddr: string, targetPeerId: string): string {
|
|
312
|
-
return `${relayAddr}/p2p-circuit/p2p/${targetPeerId}`;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
async function handleIncomingStream(
|
|
316
|
-
stream: any,
|
|
317
|
-
handlers: Map<string, MessageHandler>,
|
|
318
|
-
catchAllHandler: MessageHandler | undefined,
|
|
319
|
-
verifyFn: (signature: Uint8Array, data: Uint8Array) => Promise<boolean>
|
|
320
|
-
): Promise<void> {
|
|
321
|
-
try {
|
|
322
|
-
const chunks: Uint8Array[] = [];
|
|
323
|
-
for await (const chunk of stream.source) {
|
|
324
|
-
chunks.push(chunk.subarray());
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const data = concatUint8Arrays(chunks);
|
|
328
|
-
|
|
329
|
-
// Ignore empty streams (from unused parallel dial connections)
|
|
330
|
-
if (data.length === 0) {
|
|
331
|
-
logger.debug('Received empty stream, ignoring');
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
const envelope = decodeMessage(data);
|
|
336
|
-
|
|
337
|
-
if (!validateEnvelope(envelope)) {
|
|
338
|
-
logger.warn('Received invalid message envelope');
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// Verify signature against sender DID embedded public key.
|
|
343
|
-
const isValidSignature = await verifyEnvelope(envelope, async (signature, data) => {
|
|
344
|
-
const senderPublicKey = extractPublicKey(envelope.from);
|
|
345
|
-
return verify(signature, data, senderPublicKey);
|
|
346
|
-
});
|
|
347
|
-
if (!isValidSignature) {
|
|
348
|
-
logger.warn('Received message with invalid signature', {
|
|
349
|
-
id: envelope.id,
|
|
350
|
-
from: envelope.from,
|
|
351
|
-
});
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// Optional caller-supplied verification hook (e.g. policy checks).
|
|
356
|
-
try {
|
|
357
|
-
const { signature, ...envelopeWithoutSig } = envelope;
|
|
358
|
-
const dataBytes = new TextEncoder().encode(JSON.stringify(envelopeWithoutSig));
|
|
359
|
-
const signatureBytes = Buffer.from(signature, 'hex');
|
|
360
|
-
const hookValid = await verifyFn(signatureBytes, dataBytes);
|
|
361
|
-
if (!hookValid) {
|
|
362
|
-
logger.warn('Message rejected by custom verifier', {
|
|
363
|
-
id: envelope.id,
|
|
364
|
-
from: envelope.from,
|
|
365
|
-
});
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
} catch (error) {
|
|
369
|
-
logger.warn('Custom verification hook failed', {
|
|
370
|
-
id: envelope.id,
|
|
371
|
-
from: envelope.from,
|
|
372
|
-
error: (error as Error).message,
|
|
373
|
-
});
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
logger.info('Received message', {
|
|
378
|
-
id: envelope.id,
|
|
379
|
-
from: envelope.from,
|
|
380
|
-
to: envelope.to,
|
|
381
|
-
protocol: envelope.protocol,
|
|
382
|
-
payload: envelope.payload,
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
const handler = handlers.get(envelope.protocol);
|
|
386
|
-
let response: MessageEnvelope | void = undefined;
|
|
387
|
-
|
|
388
|
-
if (handler) {
|
|
389
|
-
response = await handler(envelope);
|
|
390
|
-
} else if (catchAllHandler) {
|
|
391
|
-
logger.debug('Using catch-all handler for protocol', { protocol: envelope.protocol });
|
|
392
|
-
response = await catchAllHandler(envelope);
|
|
393
|
-
} else {
|
|
394
|
-
logger.warn('No handler for protocol', { protocol: envelope.protocol });
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Send response back if handler returned one
|
|
398
|
-
if (response) {
|
|
399
|
-
const encoded = encodeMessage(response);
|
|
400
|
-
logger.info('Sending response back to sender', {
|
|
401
|
-
responseId: response.id,
|
|
402
|
-
replyTo: response.replyTo,
|
|
403
|
-
size: encoded.length
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
await stream.sink(
|
|
407
|
-
(async function* () {
|
|
408
|
-
yield encoded;
|
|
409
|
-
})()
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
} catch (error) {
|
|
413
|
-
logger.error('Error handling incoming message', error);
|
|
414
|
-
}
|
|
415
|
-
}
|