@synapseia-network/node 0.8.5

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.
Files changed (40) hide show
  1. package/LICENSE +105 -0
  2. package/README.md +232 -0
  3. package/dist/bid-responder-Q725ZIUC.js +86 -0
  4. package/dist/bootstrap.js +22 -0
  5. package/dist/chain-info-lightweight-2UWAQZBF.js +303 -0
  6. package/dist/chat-stream-handler-BSHSGMFF.js +127 -0
  7. package/dist/chunk-2X7MSWD4.js +270 -0
  8. package/dist/chunk-3BHRQWSM.js +531 -0
  9. package/dist/chunk-5QFTU52A.js +442 -0
  10. package/dist/chunk-5ZAJBIAV.js +25 -0
  11. package/dist/chunk-7FLDR5NT.js +186 -0
  12. package/dist/chunk-C5XRYLYP.js +137 -0
  13. package/dist/chunk-D7ADMHK2.js +36 -0
  14. package/dist/chunk-DXUYWRO7.js +23 -0
  15. package/dist/chunk-F5UDK56Z.js +289 -0
  16. package/dist/chunk-NEHR6XY7.js +111 -0
  17. package/dist/chunk-NMJVODKH.js +453 -0
  18. package/dist/chunk-PRVT22SM.js +324 -0
  19. package/dist/chunk-T2ZRG5CX.js +1380 -0
  20. package/dist/chunk-V2L5SXTL.js +88 -0
  21. package/dist/chunk-XL2NJWFY.js +702 -0
  22. package/dist/embedding-C6GE3WVM.js +16 -0
  23. package/dist/hardware-ITQQJ5YI.js +37 -0
  24. package/dist/index.js +16836 -0
  25. package/dist/inference-server-CIGRJ36H.js +25 -0
  26. package/dist/local-cors-J6RWNMMD.js +44 -0
  27. package/dist/model-catalog-C53SDFMG.js +15 -0
  28. package/dist/model-discovery-LA6YMT3I.js +10 -0
  29. package/dist/ollama-XVXA3A37.js +9 -0
  30. package/dist/rewards-vault-cli-HW7H4EMD.js +147 -0
  31. package/dist/scripts/create_nodes.sh +6 -0
  32. package/dist/scripts/diloco_train.py +319 -0
  33. package/dist/scripts/train_lora.py +237 -0
  34. package/dist/scripts/train_micro.py +586 -0
  35. package/dist/trainer-HQMV2ZAR.js +21 -0
  36. package/package.json +128 -0
  37. package/scripts/create_nodes.sh +6 -0
  38. package/scripts/diloco_train.py +319 -0
  39. package/scripts/train_lora.py +237 -0
  40. package/scripts/train_micro.py +586 -0
@@ -0,0 +1,531 @@
1
+ import { fileURLToPath as __synFup } from "url";import { dirname as __synDn } from "path";const __filename = __synFup(import.meta.url);const __dirname = __synDn(__filename);
2
+ import {
3
+ init_logger,
4
+ logger_default
5
+ } from "./chunk-V2L5SXTL.js";
6
+ import {
7
+ __esm,
8
+ __name
9
+ } from "./chunk-D7ADMHK2.js";
10
+
11
+ // src/modules/identity/identity.ts
12
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
13
+ import * as path from "path";
14
+ import * as os from "os";
15
+ import * as crypto from "crypto";
16
+ import { Injectable } from "@nestjs/common";
17
+ function _ts_decorate(decorators, target, key, desc) {
18
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
19
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
20
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
21
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
22
+ }
23
+ var IDENTITY_DIR, IDENTITY_FILE, IdentityHelper, sign2, canonicalPayload;
24
+ var init_identity = __esm({
25
+ "src/modules/identity/identity.ts"() {
26
+ "use strict";
27
+ init_logger();
28
+ __name(_ts_decorate, "_ts_decorate");
29
+ IDENTITY_DIR = process.env.SYNAPSEIA_HOME ?? path.join(os.homedir(), ".synapseia");
30
+ IDENTITY_FILE = path.join(IDENTITY_DIR, "identity.json");
31
+ IdentityHelper = class {
32
+ static {
33
+ __name(this, "IdentityHelper");
34
+ }
35
+ /**
36
+ * Generate new identity keypair using Ed25519
37
+ */
38
+ generateIdentity(identityDir = IDENTITY_DIR, nodeName) {
39
+ if (!existsSync(identityDir)) {
40
+ mkdirSync(identityDir, {
41
+ recursive: true,
42
+ mode: 448
43
+ });
44
+ }
45
+ logger_default.warn(`[Identity] Generating NEW identity (this changes peerId) \u2014 writing to ${path.join(identityDir, "identity.json")}`);
46
+ const { privateKey: privKey, publicKey: pubKey } = crypto.generateKeyPairSync("ed25519");
47
+ const privateKeyHex = privKey.export({
48
+ type: "pkcs8",
49
+ format: "der"
50
+ }).slice(-32).toString("hex");
51
+ const publicKeyHex = pubKey.export({
52
+ type: "spki",
53
+ format: "der"
54
+ }).slice(-32).toString("hex");
55
+ const peerId = publicKeyHex.slice(0, 32);
56
+ const agentId = publicKeyHex.slice(0, 8);
57
+ const identity = {
58
+ peerId,
59
+ publicKey: publicKeyHex,
60
+ privateKey: privateKeyHex,
61
+ createdAt: Date.now(),
62
+ name: nodeName,
63
+ agentId,
64
+ tier: 0,
65
+ mode: "chill",
66
+ status: "idle"
67
+ };
68
+ writeFileSync(path.join(identityDir, "identity.json"), JSON.stringify(identity, null, 2));
69
+ writeFileSync(path.join(identityDir, "publickey.pem"), `public key: ${publicKeyHex}
70
+ `);
71
+ return identity;
72
+ }
73
+ /**
74
+ * Load existing identity
75
+ */
76
+ loadIdentity(identityDir = IDENTITY_DIR) {
77
+ const idPath = path.join(identityDir, "identity.json");
78
+ if (!existsSync(idPath)) {
79
+ throw new Error(`Identity not found at ${idPath}. Run generateIdentity() or 'synapseia start' first.`);
80
+ }
81
+ const content = readFileSync(idPath, "utf-8");
82
+ const identity = JSON.parse(content);
83
+ if (!identity.peerId || !identity.publicKey || !identity.privateKey) {
84
+ throw new Error("Invalid identity file structure");
85
+ }
86
+ if (!identity.agentId) {
87
+ identity.agentId = identity.publicKey.slice(0, 8);
88
+ }
89
+ if (identity.tier === void 0) {
90
+ identity.tier = 0;
91
+ }
92
+ if (!identity.mode) {
93
+ identity.mode = "chill";
94
+ }
95
+ if (!identity.status) {
96
+ identity.status = "idle";
97
+ }
98
+ return identity;
99
+ }
100
+ /**
101
+ * Sign a message with the node's Ed25519 private key
102
+ * @param message - The message to sign (UTF-8 string)
103
+ * @param privateKeyHex - Ed25519 private key as hex string
104
+ * @returns Hex signature (64 bytes = 128 hex chars)
105
+ */
106
+ async sign(message, privateKeyHex) {
107
+ const privateKeyBytes = Buffer.from(privateKeyHex, "hex");
108
+ const messageBytes = Buffer.from(message, "utf-8");
109
+ const pkcs8Header = Buffer.from("302e020100300506032b657004220420", "hex");
110
+ const derKey = Buffer.concat([
111
+ pkcs8Header,
112
+ privateKeyBytes
113
+ ]);
114
+ const keyObject = crypto.createPrivateKey({
115
+ key: derKey,
116
+ format: "der",
117
+ type: "pkcs8"
118
+ });
119
+ const signature = crypto.sign(null, messageBytes, keyObject);
120
+ return signature.toString("hex");
121
+ }
122
+ /**
123
+ * Verify an Ed25519 signature
124
+ * @param message - The message that was signed
125
+ * @param signatureHex - The signature as hex string
126
+ * @param publicKeyHex - The Ed25519 public key as hex string
127
+ */
128
+ async verifySignature(message, signatureHex, publicKeyHex) {
129
+ try {
130
+ const messageBytes = Buffer.from(message, "utf-8");
131
+ const signatureBytes = Buffer.from(signatureHex, "hex");
132
+ const publicKeyBytes = Buffer.from(publicKeyHex, "hex");
133
+ const ED25519_DER_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
134
+ const publicKeyDer = Buffer.concat([
135
+ ED25519_DER_PREFIX,
136
+ publicKeyBytes
137
+ ]);
138
+ const keyObject = crypto.createPublicKey({
139
+ key: publicKeyDer,
140
+ format: "der",
141
+ type: "spki"
142
+ });
143
+ return crypto.verify(null, messageBytes, keyObject, signatureBytes);
144
+ } catch {
145
+ return false;
146
+ }
147
+ }
148
+ /**
149
+ * Create a canonical JSON payload for signing (keys sorted alphabetically, no signature field)
150
+ */
151
+ canonicalPayload(data) {
152
+ const { signature: _sig, ...rest } = data;
153
+ const sorted = {};
154
+ for (const key of Object.keys(rest).sort()) {
155
+ sorted[key] = rest[key];
156
+ }
157
+ return JSON.stringify(sorted);
158
+ }
159
+ /**
160
+ * Get or create identity (convenience function for CLI)
161
+ */
162
+ getOrCreateIdentity(identityDir = IDENTITY_DIR, nodeName) {
163
+ const idPath = path.join(identityDir, "identity.json");
164
+ if (existsSync(idPath)) {
165
+ const raw = readFileSync(idPath, "utf-8").trim();
166
+ if (!raw) {
167
+ throw new Error(`Identity file exists but is empty at ${idPath}. Refusing to regenerate \u2014 that would change the node's peerId and orphan its stats/rewards. Restore the file from backup OR delete it explicitly (rm "${idPath}") to create a fresh identity.`);
168
+ }
169
+ return this.loadIdentity(identityDir);
170
+ }
171
+ return this.generateIdentity(identityDir, nodeName);
172
+ }
173
+ /**
174
+ * Update identity fields (A16)
175
+ */
176
+ updateIdentity(updates, identityDir = IDENTITY_DIR) {
177
+ const identity = this.loadIdentity(identityDir);
178
+ if (updates.name !== void 0) {
179
+ identity.name = updates.name;
180
+ }
181
+ if (updates.tier !== void 0) {
182
+ identity.tier = updates.tier;
183
+ }
184
+ if (updates.mode !== void 0) {
185
+ identity.mode = updates.mode;
186
+ }
187
+ if (updates.status !== void 0) {
188
+ identity.status = updates.status;
189
+ }
190
+ writeFileSync(path.join(identityDir, "identity.json"), JSON.stringify(identity, null, 2));
191
+ return identity;
192
+ }
193
+ /**
194
+ * Get full agent profile (A16)
195
+ */
196
+ getAgentProfile(identity) {
197
+ return {
198
+ agentId: identity.agentId || identity.publicKey.slice(0, 8),
199
+ peerId: identity.peerId,
200
+ tier: identity.tier || 0,
201
+ mode: identity.mode || "chill",
202
+ status: identity.status || "idle",
203
+ createdAt: identity.createdAt,
204
+ publicKey: identity.publicKey
205
+ };
206
+ }
207
+ };
208
+ IdentityHelper = _ts_decorate([
209
+ Injectable()
210
+ ], IdentityHelper);
211
+ sign2 = /* @__PURE__ */ __name((message, privateKeyHex) => new IdentityHelper().sign(message, privateKeyHex), "sign");
212
+ canonicalPayload = /* @__PURE__ */ __name((data) => new IdentityHelper().canonicalPayload(data), "canonicalPayload");
213
+ }
214
+ });
215
+
216
+ // src/modules/p2p/p2p.ts
217
+ init_logger();
218
+ init_identity();
219
+ import { createLibp2p } from "libp2p";
220
+ import { setMaxListeners } from "events";
221
+ import * as fs from "fs";
222
+ import * as path2 from "path";
223
+ import * as os2 from "os";
224
+ import { tcp } from "@libp2p/tcp";
225
+ import { noise } from "@libp2p/noise";
226
+ import { yamux } from "@libp2p/yamux";
227
+ import { gossipsub } from "@libp2p/gossipsub";
228
+ import { kadDHT } from "@libp2p/kad-dht";
229
+ import { bootstrap } from "@libp2p/bootstrap";
230
+ import { identify } from "@libp2p/identify";
231
+ import { ping } from "@libp2p/ping";
232
+ import { Injectable as Injectable2 } from "@nestjs/common";
233
+ function _ts_decorate2(decorators, target, key, desc) {
234
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
235
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
236
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
237
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
238
+ }
239
+ __name(_ts_decorate2, "_ts_decorate");
240
+ var SYNAPSEIA_HOME = process.env.SYNAPSEIA_HOME ?? path2.join(os2.homedir(), ".synapseia");
241
+ var LIBP2P_KEY_PATH = path2.join(SYNAPSEIA_HOME, "libp2p-key");
242
+ var TOPICS = {
243
+ HEARTBEAT: "/synapseia/heartbeat/1.0.0",
244
+ SUBMISSION: "/synapseia/submission/1.0.0",
245
+ LEADERBOARD: "/synapseia/leaderboard/1.0.0",
246
+ PULSE: "/synapseia/pulse/1.0.0",
247
+ /** Auction requests from the coordinator. Nodes with `inference` capability
248
+ * listen, compute a local price via QueryCostCalculator, and publish to
249
+ * CHAT_BID. */
250
+ CHAT_AUCTION: "/synapseia/chat-auction/1.0.0",
251
+ /** Signed bids published by nodes, consumed by the coordinator. */
252
+ CHAT_BID: "/synapseia/chat-bid/1.0.0",
253
+ /** Coordinator broadcasts newly-PENDING work orders. Nodes maintain a
254
+ * short-lived in-memory queue (WorkOrderPushQueue) so the agent loop can
255
+ * skip GET /work-orders/available when push messages are flowing. The
256
+ * HTTP fetch stays as a 5-min safety-net fallback. Phase 2A. */
257
+ WORK_ORDER_AVAILABLE: "/synapseia/work-order/1.0.0",
258
+ /** Coordinator broadcasts a signed kick on every newly-INSERTed
259
+ * evaluation row so the addressed evaluator's node fires
260
+ * `kickReviewCycle` immediately. Tier 3 §3.C.1. */
261
+ EVALUATION_ASSIGNMENTS: "/synapseia/evaluation-assignments/1.0.0",
262
+ /** Coordinator broadcasts the `(peerId, shardId, expiresAt, signature)`
263
+ * ownership snapshot every 10 min (and on revocation). Nodes that
264
+ * match `peerId` upsert / drop the grant in `KgShardOwnershipStore`.
265
+ * Plan D.3 / D.4. */
266
+ KG_SHARD_OWNERSHIP: "/synapseia/kg-shard-ownership/1.0.0",
267
+ /** Coordinator publishes a redirect hint when a `/knowledge-graph/query`
268
+ * request maps to a shard hosted off-coord. Authorised hosts dial the
269
+ * requester directly over `/synapseia/kg-shard-query/1.0.0`. Plan
270
+ * D.3 / D.4. */
271
+ KG_QUERY_REDIRECT: "/synapseia/kg-query-redirect/1.0.0",
272
+ /** A node announces "I have shard S persisted and ready" after a
273
+ * successful snapshot pull. Other peers add the announcer to their
274
+ * in-memory `Map<shardId, Set<peerId>>` so future cold-boot peers
275
+ * dial them first (chained sync) and coord uplink stays ≈ zero.
276
+ * Signed by the NODE (not coord); envelope carries the full 32-byte
277
+ * pubkeyHex so the verifier can check `pubkeyHex.startsWith(peerId)`.
278
+ * Plan D.4-distribution.5. */
279
+ KG_SHARD_SNAPSHOT_READY: "/synapseia/kg-shard-snapshot-ready/1.0.0",
280
+ /** Coordinator broadcasts every newly-inserted PubMedBERT 768d
281
+ * embedding as a signed batched envelope. Authorised hosts append
282
+ * to local shard storage and (later) the live HNSW index. Coord
283
+ * rate-limits to ≤ MAX_BATCH=50 records / envelope; topic is global
284
+ * (every peer subscribes and filters by owned shard set). Plan
285
+ * D.4-distribution.6/7/8. */
286
+ KG_EMBEDDING_DELTA: "/synapseia/kg-embedding-delta/1.0.0"
287
+ };
288
+ var CHAT_PROTOCOL = "/synapseia/chat/1.0.0";
289
+ var P2PNode = class {
290
+ static {
291
+ __name(this, "P2PNode");
292
+ }
293
+ identity;
294
+ node = null;
295
+ handlers = /* @__PURE__ */ new Map();
296
+ rawHandlers = /* @__PURE__ */ new Map();
297
+ constructor(identity) {
298
+ this.identity = identity;
299
+ }
300
+ async start(bootstrapAddrs = []) {
301
+ const keysModule = await import("@libp2p/crypto/keys");
302
+ const privateKey = await this.loadOrCreateKey(keysModule);
303
+ const svcBase = {
304
+ identify: identify(),
305
+ ping: ping(),
306
+ pubsub: gossipsub({
307
+ allowPublishToZeroTopicPeers: true,
308
+ emitSelf: false
309
+ }),
310
+ dht: kadDHT({
311
+ clientMode: bootstrapAddrs.length === 0
312
+ })
313
+ };
314
+ const svc = bootstrapAddrs.length > 0 ? {
315
+ ...svcBase,
316
+ bootstrap: bootstrap({
317
+ list: bootstrapAddrs
318
+ })
319
+ } : svcBase;
320
+ this.node = await createLibp2p({
321
+ privateKey,
322
+ transports: [
323
+ tcp()
324
+ ],
325
+ connectionEncrypters: [
326
+ noise()
327
+ ],
328
+ streamMuxers: [
329
+ yamux()
330
+ ],
331
+ services: svc
332
+ });
333
+ await this.node.start();
334
+ this.node.services.pubsub.addEventListener("message", (evt) => {
335
+ const { topic, data, from } = evt.detail;
336
+ const peer = from?.toString() ?? "unknown";
337
+ const rawCbs = this.rawHandlers.get(topic) ?? [];
338
+ for (const cb of rawCbs) {
339
+ try {
340
+ void cb(data, peer);
341
+ } catch {
342
+ }
343
+ }
344
+ try {
345
+ const parsed = JSON.parse(new TextDecoder().decode(data));
346
+ for (const cb of this.handlers.get(topic) ?? []) {
347
+ cb(parsed, peer);
348
+ }
349
+ } catch {
350
+ }
351
+ });
352
+ for (const t of Object.values(TOPICS)) {
353
+ this.node.services.pubsub.subscribe(t);
354
+ }
355
+ const peerId = this.node.peerId.toString();
356
+ const addrs = this.node.getMultiaddrs().map((a) => a.toString());
357
+ logger_default.log("[P2P] Node started | peerId:", peerId);
358
+ if (addrs.length > 0) logger_default.log("[P2P] Listening on:", addrs.join(", "));
359
+ try {
360
+ setMaxListeners(20, this.node);
361
+ logger_default.log(`[P2P] setMaxListeners(20) on node (ping service uses AbortSignals)`);
362
+ } catch (err) {
363
+ }
364
+ this.node.addEventListener("peer:discovery", (evt) => {
365
+ const id = evt.detail?.id?.toString() ?? "unknown";
366
+ logger_default.log("[P2P] \u{1F50D} Peer discovered:", id);
367
+ });
368
+ this.node.addEventListener("peer:connect", (evt) => {
369
+ const id = evt.detail?.toString() ?? "unknown";
370
+ logger_default.log("[P2P] \u2705 Peer connected:", id);
371
+ });
372
+ this.node.addEventListener("peer:disconnect", (evt) => {
373
+ const id = evt.detail?.toString() ?? "unknown";
374
+ logger_default.log("[P2P] \u274C Peer disconnected:", id);
375
+ });
376
+ }
377
+ async loadOrCreateKey(keys) {
378
+ try {
379
+ const fileContent = fs.readFileSync(LIBP2P_KEY_PATH, "utf8").trim();
380
+ let bytes;
381
+ try {
382
+ bytes = Buffer.from(fileContent, "hex");
383
+ } catch {
384
+ bytes = Buffer.from(fileContent, "binary");
385
+ }
386
+ const key = keys.privateKeyFromProtobuf(bytes);
387
+ logger_default.log(`[P2P] loaded persistent identity from ${LIBP2P_KEY_PATH}`);
388
+ return key;
389
+ } catch (err) {
390
+ if (err?.code !== "ENOENT") {
391
+ logger_default.warn(`[P2P] could not read ${LIBP2P_KEY_PATH} (${err.message}) \u2014 generating fresh key`);
392
+ }
393
+ const key = await keys.generateKeyPair("Ed25519");
394
+ try {
395
+ fs.mkdirSync(path2.dirname(LIBP2P_KEY_PATH), {
396
+ recursive: true,
397
+ mode: 448
398
+ });
399
+ const protoBytes = keys.privateKeyToProtobuf(key);
400
+ fs.writeFileSync(LIBP2P_KEY_PATH, Buffer.from(protoBytes).toString("hex"), {
401
+ mode: 384
402
+ });
403
+ logger_default.log(`[P2P] generated + persisted new identity at ${LIBP2P_KEY_PATH} \u2014 restarts will keep the same peerId`);
404
+ } catch (writeErr) {
405
+ logger_default.warn(`[P2P] generated key but FAILED to persist at ${LIBP2P_KEY_PATH} (${writeErr.message}) \u2014 peerId will change on next restart. Check the volume mount.`);
406
+ }
407
+ return key;
408
+ }
409
+ }
410
+ async stop() {
411
+ if (this.node) {
412
+ await this.node.stop();
413
+ this.node = null;
414
+ logger_default.log("[P2P] Node stopped");
415
+ }
416
+ }
417
+ isRunning() {
418
+ return this.node !== null;
419
+ }
420
+ getPeerId() {
421
+ if (!this.node) return this.identity.peerId;
422
+ return this.node.peerId.toString();
423
+ }
424
+ getConnectedPeers() {
425
+ if (!this.node) return [];
426
+ return this.node.getPeers().map((p) => p.toString());
427
+ }
428
+ getMultiaddrs() {
429
+ if (!this.node) return [];
430
+ return this.node.getMultiaddrs().map((a) => a.toString());
431
+ }
432
+ onMessage(topic, cb) {
433
+ const existing = this.handlers.get(topic) ?? [];
434
+ this.handlers.set(topic, [
435
+ ...existing,
436
+ cb
437
+ ]);
438
+ }
439
+ /**
440
+ * Subscribe to raw gossipsub message bytes for a topic. Used by paths
441
+ * that need the original wire payload (e.g. signed-envelope verifiers
442
+ * that must reconstruct the exact bytes the publisher signed).
443
+ */
444
+ onRawMessage(topic, cb) {
445
+ const existing = this.rawHandlers.get(topic) ?? [];
446
+ this.rawHandlers.set(topic, [
447
+ ...existing,
448
+ cb
449
+ ]);
450
+ }
451
+ async publish(topic, data) {
452
+ if (!this.node) throw new Error("P2P node not started");
453
+ const encoded = new TextEncoder().encode(JSON.stringify(data));
454
+ try {
455
+ await this.node.services.pubsub.publish(topic, encoded);
456
+ } catch (err) {
457
+ const name = err?.name ?? "";
458
+ const msg = err?.message ?? String(err);
459
+ if (name === "StreamStateError" || /closed|stream/i.test(msg)) {
460
+ logger_default.debug?.(`[p2p] publish to ${topic} dropped (peer stream closed): ${msg}`);
461
+ return;
462
+ }
463
+ throw err;
464
+ }
465
+ }
466
+ async publishHeartbeat(data) {
467
+ const payload = {
468
+ ...data
469
+ };
470
+ const canonical = canonicalPayload(payload);
471
+ const signature = await sign2(canonical, this.identity.privateKey);
472
+ return this.publish(TOPICS.HEARTBEAT, {
473
+ ...payload,
474
+ signature,
475
+ publicKey: this.identity.publicKey
476
+ });
477
+ }
478
+ async publishSubmission(data) {
479
+ return this.publish(TOPICS.SUBMISSION, data);
480
+ }
481
+ /**
482
+ * Register an inbound libp2p protocol handler. libp2p v3 calls the
483
+ * handler with TWO POSITIONAL arguments — `(stream, connection)` — not
484
+ * an object. Passing a `(ctx) => ctx.stream` handler silently yields
485
+ * `undefined` on every inbound stream (ctx is actually the Stream
486
+ * itself; no `stream` property exists), so our codec call crashes on
487
+ * `for await (const chunk of undefined)` and the peer times out.
488
+ */
489
+ async handleProtocol(protocol, handler) {
490
+ if (!this.node) throw new Error("P2P node not started");
491
+ await this.node.handle(protocol, handler);
492
+ }
493
+ /** Access the raw libp2p node — only for helpers that need fine-grained API. */
494
+ getNode() {
495
+ return this.node;
496
+ }
497
+ /**
498
+ * Dial a full multiaddr (`/dns4/.../tcp/9000/p2p/<peerId>`). Used by the
499
+ * coord-reconnect watchdog in node-runtime when the coord's libp2p peerId
500
+ * has changed (coord restart without persisted identity, or the volume
501
+ * was wiped). Returns silently on failure so the watchdog can retry.
502
+ */
503
+ async dial(multiaddr) {
504
+ if (!this.node) throw new Error("P2P node not started");
505
+ const { multiaddr: ma } = await import("@multiformats/multiaddr");
506
+ await this.node.dial(ma(multiaddr));
507
+ }
508
+ };
509
+ var P2pHelper = class {
510
+ static {
511
+ __name(this, "P2pHelper");
512
+ }
513
+ async createP2PNode(identity, bootstrapAddrs = []) {
514
+ const node = new P2PNode(identity);
515
+ await node.start(bootstrapAddrs);
516
+ return node;
517
+ }
518
+ };
519
+ P2pHelper = _ts_decorate2([
520
+ Injectable2()
521
+ ], P2pHelper);
522
+
523
+ export {
524
+ IdentityHelper,
525
+ sign2 as sign,
526
+ canonicalPayload,
527
+ init_identity,
528
+ TOPICS,
529
+ CHAT_PROTOCOL,
530
+ P2pHelper
531
+ };