@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@highway1/core",
3
- "version": "0.1.48",
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
- "uint8arrays": "^5.1.0"
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/node.js';
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/dht.js';
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';
@@ -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 { DHTOperations } from '../discovery/dht.js';
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, peerHint?: PeerHint) => Promise<MessageEnvelope | void>;
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 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.
26
+ * Create a message router using relay client (CVP-0011)
52
27
  */
53
28
  export function createMessageRouter(
54
- libp2p: Libp2p,
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
- const PROTOCOL_PREFIX = '/clawiverse/msg/1.0.0';
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, peerHint?: PeerHint) => {
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 stream.sink(
227
- (async function* () {
228
- yield encoded;
229
- })()
230
- );
64
+ await relayClient.sendEnvelope(envelope.to, encoded);
231
65
 
232
- logger.info('Message sent over libp2p stream', {
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
- try {
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
- const responsePromise = (async () => {
248
- const responseChunks: Uint8Array[] = [];
249
- for await (const chunk of stream.source) {
250
- responseChunks.push(chunk.subarray());
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
- if (responseChunks.length > 0) {
254
- const responseData = concatUint8Arrays(responseChunks);
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
- await libp2p.handle(PROTOCOL_PREFIX, async ({ stream }) => {
98
+ // Register handler for incoming DELIVER messages
99
+ relayClient.onDeliver(async (deliverMsg) => {
292
100
  try {
293
- await handleIncomingStream(stream, handlers, catchAllHandler, verifyFn);
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 stream', error);
189
+ logger.error('Error handling incoming message', error);
296
190
  }
297
- }, { runOnLimitedConnection: true });
191
+ });
298
192
 
299
193
  logger.info('Message router started');
300
194
  },
301
195
 
302
196
  stop: async () => {
303
- await libp2p.unhandle(PROTOCOL_PREFIX);
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
- }