@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.
@@ -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,2 @@
1
+ export * from './node.js';
2
+ export * from './connection.js';
@@ -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
+ }