@decentnetwork/lan 0.1.0

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 (79) hide show
  1. package/LICENSE +31 -0
  2. package/README.md +296 -0
  3. package/bin/tun-helper-darwin-amd64 +0 -0
  4. package/bin/tun-helper-darwin-arm64 +0 -0
  5. package/bin/tun-helper-linux-amd64 +0 -0
  6. package/bin/tun-helper-linux-arm64 +0 -0
  7. package/dist/acl/acl-engine.d.ts +43 -0
  8. package/dist/acl/acl-engine.js +189 -0
  9. package/dist/acl/audit.d.ts +70 -0
  10. package/dist/acl/audit.js +144 -0
  11. package/dist/acl/index.d.ts +4 -0
  12. package/dist/acl/index.js +3 -0
  13. package/dist/acl/policy.d.ts +31 -0
  14. package/dist/acl/policy.js +102 -0
  15. package/dist/acl/types.d.ts +18 -0
  16. package/dist/acl/types.js +4 -0
  17. package/dist/carrier/frame.d.ts +18 -0
  18. package/dist/carrier/frame.js +66 -0
  19. package/dist/carrier/index.d.ts +5 -0
  20. package/dist/carrier/index.js +4 -0
  21. package/dist/carrier/packet-session.d.ts +32 -0
  22. package/dist/carrier/packet-session.js +151 -0
  23. package/dist/carrier/peer-manager.d.ts +113 -0
  24. package/dist/carrier/peer-manager.js +392 -0
  25. package/dist/carrier/types.d.ts +10 -0
  26. package/dist/carrier/types.js +11 -0
  27. package/dist/cli/commands.d.ts +223 -0
  28. package/dist/cli/commands.js +932 -0
  29. package/dist/cli/index.d.ts +7 -0
  30. package/dist/cli/index.js +196 -0
  31. package/dist/config/loader.d.ts +10 -0
  32. package/dist/config/loader.js +152 -0
  33. package/dist/daemon/index.d.ts +1 -0
  34. package/dist/daemon/index.js +1 -0
  35. package/dist/daemon/ipc.d.ts +60 -0
  36. package/dist/daemon/ipc.js +144 -0
  37. package/dist/daemon/server.d.ts +63 -0
  38. package/dist/daemon/server.js +510 -0
  39. package/dist/dns/index.d.ts +1 -0
  40. package/dist/dns/index.js +1 -0
  41. package/dist/dns/resolver.d.ts +44 -0
  42. package/dist/dns/resolver.js +82 -0
  43. package/dist/dns/server.d.ts +70 -0
  44. package/dist/dns/server.js +393 -0
  45. package/dist/dora/dora-integration.d.ts +90 -0
  46. package/dist/dora/dora-integration.js +325 -0
  47. package/dist/index.d.ts +13 -0
  48. package/dist/index.js +15 -0
  49. package/dist/ipam/index.d.ts +1 -0
  50. package/dist/ipam/index.js +1 -0
  51. package/dist/ipam/ipam.d.ts +99 -0
  52. package/dist/ipam/ipam.js +254 -0
  53. package/dist/proxy/connect-proxy.d.ts +78 -0
  54. package/dist/proxy/connect-proxy.js +204 -0
  55. package/dist/router/index.d.ts +5 -0
  56. package/dist/router/index.js +4 -0
  57. package/dist/router/ip-parser.d.ts +36 -0
  58. package/dist/router/ip-parser.js +127 -0
  59. package/dist/router/packet-router.d.ts +49 -0
  60. package/dist/router/packet-router.js +251 -0
  61. package/dist/router/session-manager.d.ts +50 -0
  62. package/dist/router/session-manager.js +138 -0
  63. package/dist/router/types.d.ts +21 -0
  64. package/dist/router/types.js +6 -0
  65. package/dist/tun/index.d.ts +3 -0
  66. package/dist/tun/index.js +2 -0
  67. package/dist/tun/route-manager.d.ts +59 -0
  68. package/dist/tun/route-manager.js +353 -0
  69. package/dist/tun/tun-device.d.ts +45 -0
  70. package/dist/tun/tun-device.js +265 -0
  71. package/dist/tun/types.d.ts +28 -0
  72. package/dist/tun/types.js +4 -0
  73. package/dist/types.d.ts +176 -0
  74. package/dist/types.js +4 -0
  75. package/dist/utils/logger.d.ts +20 -0
  76. package/dist/utils/logger.js +43 -0
  77. package/docs/CONFIGURATION.md +197 -0
  78. package/docs/INSTALL.md +145 -0
  79. package/package.json +93 -0
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Manages Carrier peer identity and connections
3
+ * Wraps @decentnetwork/peer SDK
4
+ */
5
+ import { EventEmitter } from "events";
6
+ import { PacketSession } from "./packet-session.js";
7
+ import type { PeerIdentity, RemotePeer } from "./types.js";
8
+ import type { BootstrapNode } from "../types.js";
9
+ export interface PeerManagerOptions {
10
+ keyFile: string;
11
+ bootstrapNodes: BootstrapNode[];
12
+ expressNodes?: BootstrapNode[];
13
+ }
14
+ export declare class PeerManager extends EventEmitter {
15
+ private peer;
16
+ private identity;
17
+ private sessions;
18
+ private sessionCounter;
19
+ private logger;
20
+ constructor();
21
+ create(opts: PeerManagerOptions): Promise<void>;
22
+ start(): Promise<void>;
23
+ joinNetwork(): Promise<void>;
24
+ announceSelf(timeoutMs?: number): Promise<void>;
25
+ /**
26
+ * Accept a pending friend request while the daemon is running.
27
+ * Replaces the daemon-down `friend-accept --wait` ceremony — the
28
+ * running daemon can just call this in response to an incoming
29
+ * onFriendRequest event. Idempotent: re-accepting an already-accepted
30
+ * pubkey is a no-op at the SDK level.
31
+ */
32
+ acceptFriendRequest(pubkey: string): Promise<void>;
33
+ /**
34
+ * Send an outbound friend request to a Carrier address (NOT a bare
35
+ * userid — sendFriendRequest needs the address form because it
36
+ * includes the recipient's nospam token). Used by DoraIntegration
37
+ * to auto-friend every peer in the roster.
38
+ *
39
+ * The SDK is idempotent for already-accepted peers (it short-
40
+ * circuits via the `acceptedAt` cache), so callers can spam this
41
+ * on every roster refresh without re-prompting iOS Beagle users.
42
+ */
43
+ sendFriendRequest(address: string, hello?: string): Promise<void>;
44
+ /**
45
+ * Returns true if the given userid is already an accepted friend of
46
+ * this peer. Lets DoraIntegration skip sendFriendRequest for peers
47
+ * we've already established a session with.
48
+ */
49
+ isFriend(userid: string): boolean;
50
+ stop(): Promise<void>;
51
+ getIdentity(): PeerIdentity;
52
+ /**
53
+ * Returns the userid (base58) — this is the identifier used in the
54
+ * friend store, in onText callbacks, and as the argument to sendText().
55
+ * Despite the historical name "pubkey", this is NOT the hex public key.
56
+ */
57
+ getPubkey(): string;
58
+ /**
59
+ * Returns the hex-encoded raw public key (32 bytes hex).
60
+ * Use this for cryptographic operations, not for friend lookups.
61
+ */
62
+ getHexPubkey(): string;
63
+ getAddress(): string;
64
+ getFriends(): RemotePeer[];
65
+ /**
66
+ * Check if a specific friend is currently online (direct UDP path established).
67
+ * Returns false for unknown pubkeys.
68
+ */
69
+ isFriendOnline(pubkey: string): boolean;
70
+ /**
71
+ * Actively wait for a friend to come online. The SDK uses this signal to
72
+ * accelerate route discovery + UDP holepunching for that friend.
73
+ * Returns true if the friend went online within the timeout.
74
+ */
75
+ waitForFriendConnected(pubkey: string, timeoutMs?: number): Promise<boolean>;
76
+ /**
77
+ * Trigger the SDK's session-establishment path for a peer by issuing a
78
+ * dummy sendText. peer.sendText() internally calls #initiateSession when
79
+ * no session exists. We swallow the resulting "friend offline" error.
80
+ *
81
+ * This is a workaround for the SDK quirk where one side's friendOnline
82
+ * notification never fires, leaving the friend perpetually "offline" even
83
+ * when both sides are connected to the same TCP relays.
84
+ */
85
+ kickSessionEstablishment(pubkey: string): Promise<void>;
86
+ /**
87
+ * Send a raw text payload to a friend. Used by application-layer
88
+ * protocols (e.g. dora) that ride on the same Carrier text channel as
89
+ * our base64-encoded packet frames but carry a different wire prefix
90
+ * (DORA:). Packet-router code never calls this directly — it goes
91
+ * through PacketSession.
92
+ */
93
+ sendText(pubkey: string, text: string): Promise<void>;
94
+ /**
95
+ * Open an outbound packet session (initiator).
96
+ * Sends HANDSHAKE_REQ and waits for HANDSHAKE_ACK.
97
+ */
98
+ openPacketSession(pubkey: string): Promise<PacketSession>;
99
+ closePacketSession(pubkey: string): void;
100
+ /**
101
+ * Get an existing session for a peer (for routing logic).
102
+ */
103
+ getSession(pubkey: string): PacketSession | null;
104
+ /**
105
+ * Internal: Create and register a session.
106
+ */
107
+ private createSession;
108
+ /**
109
+ * Send a HANDSHAKE_ACK frame to a peer.
110
+ */
111
+ private sendHandshakeAck;
112
+ private setupEventHandlers;
113
+ }
@@ -0,0 +1,392 @@
1
+ /**
2
+ * Manages Carrier peer identity and connections
3
+ * Wraps @decentnetwork/peer SDK
4
+ */
5
+ import { Peer } from "@decentnetwork/peer";
6
+ import { EventEmitter } from "events";
7
+ import { PacketSession } from "./packet-session.js";
8
+ import { FrameCodec } from "./frame.js";
9
+ import { FRAME_OPCODE_HANDSHAKE_ACK, } from "./types.js";
10
+ import { Logger } from "../utils/logger.js";
11
+ // Dora wire-prefix. Kept inline (rather than imported from @decentnetwork/dora) to
12
+ // avoid coupling peer-manager — a transport-layer module — to an
13
+ // application-protocol package. The prefix is part of decentlan's text
14
+ // channel contract and must match the constant defined in dora's types.ts.
15
+ const DORA_PREFIX = "DORA:";
16
+ export class PeerManager extends EventEmitter {
17
+ peer = null;
18
+ identity = null;
19
+ sessions = new Map();
20
+ sessionCounter = 1;
21
+ logger;
22
+ constructor() {
23
+ super();
24
+ this.logger = new Logger({ prefix: "PeerManager" });
25
+ }
26
+ async create(opts) {
27
+ this.logger.info("Creating Peer instance");
28
+ this.peer = await Peer.create({
29
+ keyFile: opts.keyFile,
30
+ compatibilityMode: "legacy",
31
+ bootstrapNodes: opts.bootstrapNodes,
32
+ expressNodes: opts.expressNodes,
33
+ });
34
+ this.setupEventHandlers();
35
+ this.logger.info("Peer instance created (not yet started)");
36
+ }
37
+ async start() {
38
+ if (!this.peer) {
39
+ throw new Error("Peer not created. Call create() first.");
40
+ }
41
+ this.logger.info("Starting Peer");
42
+ await this.peer.start();
43
+ // Identity is only available after start()
44
+ this.identity = {
45
+ pubkey: this.peer.pubkey(),
46
+ address: this.peer.address(),
47
+ userid: this.peer.userid(),
48
+ };
49
+ this.logger.info(`Peer started: ${this.identity.address}`);
50
+ }
51
+ async joinNetwork() {
52
+ if (!this.peer) {
53
+ throw new Error("Peer not created. Call create() first.");
54
+ }
55
+ this.logger.info("Joining Carrier network");
56
+ await this.peer.joinNetwork();
57
+ this.logger.info("Joined Carrier network");
58
+ }
59
+ async announceSelf(timeoutMs) {
60
+ if (!this.peer) {
61
+ throw new Error("Peer not created. Call create() first.");
62
+ }
63
+ this.logger.info("Announcing self on DHT");
64
+ await this.peer.announceSelf(timeoutMs);
65
+ this.logger.info("Announced self on DHT");
66
+ }
67
+ /**
68
+ * Accept a pending friend request while the daemon is running.
69
+ * Replaces the daemon-down `friend-accept --wait` ceremony — the
70
+ * running daemon can just call this in response to an incoming
71
+ * onFriendRequest event. Idempotent: re-accepting an already-accepted
72
+ * pubkey is a no-op at the SDK level.
73
+ */
74
+ async acceptFriendRequest(pubkey) {
75
+ if (!this.peer) {
76
+ throw new Error("Peer not created. Call create() first.");
77
+ }
78
+ await this.peer.acceptFriendRequest(pubkey);
79
+ this.logger.info(`Accepted friend request from ${pubkey}`);
80
+ }
81
+ /**
82
+ * Send an outbound friend request to a Carrier address (NOT a bare
83
+ * userid — sendFriendRequest needs the address form because it
84
+ * includes the recipient's nospam token). Used by DoraIntegration
85
+ * to auto-friend every peer in the roster.
86
+ *
87
+ * The SDK is idempotent for already-accepted peers (it short-
88
+ * circuits via the `acceptedAt` cache), so callers can spam this
89
+ * on every roster refresh without re-prompting iOS Beagle users.
90
+ */
91
+ async sendFriendRequest(address, hello) {
92
+ if (!this.peer) {
93
+ throw new Error("Peer not created. Call create() first.");
94
+ }
95
+ await this.peer.sendFriendRequest(address, hello);
96
+ }
97
+ /**
98
+ * Returns true if the given userid is already an accepted friend of
99
+ * this peer. Lets DoraIntegration skip sendFriendRequest for peers
100
+ * we've already established a session with.
101
+ */
102
+ isFriend(userid) {
103
+ if (!this.peer)
104
+ return false;
105
+ // FriendRecord stores both pubkey (hex) and userid (base58). The
106
+ // dora roster gives us userid; match on either form to be safe
107
+ // against older friends.json entries that only set `pubkey`.
108
+ return this.peer
109
+ .friends()
110
+ .some((f) => f.userid === userid || f.pubkey === userid);
111
+ }
112
+ async stop() {
113
+ if (!this.peer) {
114
+ return;
115
+ }
116
+ this.logger.info("Stopping Peer");
117
+ // Close all sessions
118
+ for (const session of this.sessions.values()) {
119
+ session.close();
120
+ }
121
+ this.sessions.clear();
122
+ await this.peer.stop();
123
+ this.logger.info("Peer stopped");
124
+ }
125
+ getIdentity() {
126
+ if (!this.identity) {
127
+ throw new Error("Peer not created. Call create() first.");
128
+ }
129
+ return this.identity;
130
+ }
131
+ /**
132
+ * Returns the userid (base58) — this is the identifier used in the
133
+ * friend store, in onText callbacks, and as the argument to sendText().
134
+ * Despite the historical name "pubkey", this is NOT the hex public key.
135
+ */
136
+ getPubkey() {
137
+ if (!this.peer) {
138
+ throw new Error("Peer not created. Call create() first.");
139
+ }
140
+ return this.peer.userid();
141
+ }
142
+ /**
143
+ * Returns the hex-encoded raw public key (32 bytes hex).
144
+ * Use this for cryptographic operations, not for friend lookups.
145
+ */
146
+ getHexPubkey() {
147
+ if (!this.peer) {
148
+ throw new Error("Peer not created. Call create() first.");
149
+ }
150
+ return this.peer.pubkey();
151
+ }
152
+ getAddress() {
153
+ if (!this.peer) {
154
+ throw new Error("Peer not created. Call create() first.");
155
+ }
156
+ return this.peer.address();
157
+ }
158
+ getFriends() {
159
+ if (!this.peer) {
160
+ throw new Error("Peer not created. Call create() first.");
161
+ }
162
+ return this.peer.friends().map((friend) => ({
163
+ pubkey: friend.pubkey,
164
+ name: friend.name,
165
+ status: friend.status === "online" ? "online" : "offline",
166
+ lastSeen: friend.acceptedAt,
167
+ }));
168
+ }
169
+ /**
170
+ * Check if a specific friend is currently online (direct UDP path established).
171
+ * Returns false for unknown pubkeys.
172
+ */
173
+ isFriendOnline(pubkey) {
174
+ if (!this.peer)
175
+ return false;
176
+ const friend = this.peer.friends().find((f) => f.pubkey === pubkey);
177
+ return friend?.status === "online";
178
+ }
179
+ /**
180
+ * Actively wait for a friend to come online. The SDK uses this signal to
181
+ * accelerate route discovery + UDP holepunching for that friend.
182
+ * Returns true if the friend went online within the timeout.
183
+ */
184
+ async waitForFriendConnected(pubkey, timeoutMs = 60000) {
185
+ if (!this.peer)
186
+ return false;
187
+ return this.peer.waitForFriendConnected(pubkey, timeoutMs);
188
+ }
189
+ /**
190
+ * Trigger the SDK's session-establishment path for a peer by issuing a
191
+ * dummy sendText. peer.sendText() internally calls #initiateSession when
192
+ * no session exists. We swallow the resulting "friend offline" error.
193
+ *
194
+ * This is a workaround for the SDK quirk where one side's friendOnline
195
+ * notification never fires, leaving the friend perpetually "offline" even
196
+ * when both sides are connected to the same TCP relays.
197
+ */
198
+ async kickSessionEstablishment(pubkey) {
199
+ if (!this.peer)
200
+ return;
201
+ try {
202
+ await this.peer.sendText(pubkey, "");
203
+ }
204
+ catch {
205
+ // expected when friend isn't online yet — the call still triggered
206
+ // #initiateSession internally, which is the side effect we want.
207
+ }
208
+ }
209
+ /**
210
+ * Send a raw text payload to a friend. Used by application-layer
211
+ * protocols (e.g. dora) that ride on the same Carrier text channel as
212
+ * our base64-encoded packet frames but carry a different wire prefix
213
+ * (DORA:). Packet-router code never calls this directly — it goes
214
+ * through PacketSession.
215
+ */
216
+ async sendText(pubkey, text) {
217
+ if (!this.peer) {
218
+ throw new Error("Peer not created. Call create() first.");
219
+ }
220
+ await this.peer.sendText(pubkey, text);
221
+ }
222
+ /**
223
+ * Open an outbound packet session (initiator).
224
+ * Sends HANDSHAKE_REQ and waits for HANDSHAKE_ACK.
225
+ */
226
+ async openPacketSession(pubkey) {
227
+ const existing = this.sessions.get(pubkey);
228
+ if (existing && existing.isConnected()) {
229
+ return existing;
230
+ }
231
+ const sessionId = this.sessionCounter++;
232
+ const session = this.createSession(pubkey, sessionId);
233
+ try {
234
+ await session.handshake();
235
+ }
236
+ catch (error) {
237
+ this.sessions.delete(pubkey);
238
+ throw error;
239
+ }
240
+ // Emit session-opened so PacketRouter can attach its "packet" listener.
241
+ // The responder side already emits this when handling handshake-req;
242
+ // the initiator side needs to emit it after the handshake completes.
243
+ this.emit("session-opened", { pubkey, sessionId });
244
+ return session;
245
+ }
246
+ closePacketSession(pubkey) {
247
+ const session = this.sessions.get(pubkey);
248
+ if (session) {
249
+ session.close();
250
+ this.sessions.delete(pubkey);
251
+ }
252
+ }
253
+ /**
254
+ * Get an existing session for a peer (for routing logic).
255
+ */
256
+ getSession(pubkey) {
257
+ return this.sessions.get(pubkey) || null;
258
+ }
259
+ /**
260
+ * Internal: Create and register a session.
261
+ */
262
+ createSession(pubkey, sessionId) {
263
+ const session = new PacketSession({
264
+ peerId: pubkey,
265
+ sessionId,
266
+ sendText: async (text) => {
267
+ if (!this.peer) {
268
+ throw new Error("Peer not available");
269
+ }
270
+ await this.peer.sendText(pubkey, text);
271
+ },
272
+ onClose: () => {
273
+ this.sessions.delete(pubkey);
274
+ },
275
+ });
276
+ // Listen for handshake requests on this session — auto-respond with ACK
277
+ session.on("handshake-req", async () => {
278
+ this.logger.info(`Received HANDSHAKE_REQ from ${pubkey}, sending ACK`);
279
+ try {
280
+ await this.sendHandshakeAck(pubkey, sessionId);
281
+ session.acceptIncoming(); // Mark session as active (responder side)
282
+ this.emit("session-opened", { pubkey, sessionId });
283
+ }
284
+ catch (error) {
285
+ this.logger.error(`Failed to send HANDSHAKE_ACK to ${pubkey}:`, error);
286
+ }
287
+ });
288
+ this.sessions.set(pubkey, session);
289
+ return session;
290
+ }
291
+ /**
292
+ * Send a HANDSHAKE_ACK frame to a peer.
293
+ */
294
+ async sendHandshakeAck(pubkey, sessionId) {
295
+ if (!this.peer) {
296
+ throw new Error("Peer not available");
297
+ }
298
+ const frame = FrameCodec.encode(new Uint8Array(0), sessionId, FRAME_OPCODE_HANDSHAKE_ACK);
299
+ const frameBase64 = Buffer.from(frame).toString("base64");
300
+ await this.peer.sendText(pubkey, frameBase64);
301
+ }
302
+ setupEventHandlers() {
303
+ if (!this.peer) {
304
+ return;
305
+ }
306
+ // Friend connection events
307
+ this.peer.onFriendConnection((event) => {
308
+ const evt = {
309
+ pubkey: event.pubkey,
310
+ status: event.status === "connected" ? "connected" : "disconnected",
311
+ timestamp: Date.now(),
312
+ };
313
+ this.emit("friend-connection", evt);
314
+ this.logger.info(`Friend ${event.pubkey} ${event.status}`);
315
+ // If a friend goes offline (or, more importantly, comes BACK
316
+ // online after we already had a session — e.g. they restarted
317
+ // their daemon), the Carrier crypto session under us has
318
+ // rotated. Our cached PacketSession is bound to the previous
319
+ // session's frame-sequence state. Sending DATA frames through
320
+ // it produces ciphertext the new peer can't decrypt; the
321
+ // packets land in their PeerManager.onText but no session
322
+ // matches and they're dropped silently. Result: 100% loss
323
+ // that looks like "the link is up but pings time out."
324
+ //
325
+ // Fix: drop our PacketSession on every friend-status change.
326
+ // The next outbound packet triggers a fresh handshake (or the
327
+ // remote auto-creates from our HANDSHAKE_REQ); fast, clean,
328
+ // no stale-session lingering.
329
+ const existing = this.sessions.get(event.pubkey);
330
+ if (existing) {
331
+ this.logger.debug(`Closing stale packet session to ${event.pubkey} on friend-${event.status}`);
332
+ existing.close();
333
+ this.sessions.delete(event.pubkey);
334
+ }
335
+ });
336
+ // Text messages contain our framed packets
337
+ this.peer.onText((message) => {
338
+ try {
339
+ // Silently skip empty messages — kickSessionEstablishment sends
340
+ // an empty sendText to trigger the SDK's #initiateSession path,
341
+ // and the receiver doesn't need to log a warning for that.
342
+ if (message.text.length === 0) {
343
+ return;
344
+ }
345
+ // Dora messages share the same text channel as packet frames but
346
+ // carry a `DORA:` ASCII prefix that's not valid base64. Route them
347
+ // to whoever is subscribed (DoraClient/DoraServer in daemon).
348
+ if (message.text.startsWith(DORA_PREFIX)) {
349
+ this.emit("dora-message", message.pubkey, message.text);
350
+ return;
351
+ }
352
+ const frameData = Buffer.from(message.text, "base64");
353
+ const frame = FrameCodec.decode(new Uint8Array(frameData));
354
+ if (!frame) {
355
+ this.logger.warn(`Invalid frame from ${message.pubkey}`);
356
+ return;
357
+ }
358
+ let session = this.sessions.get(message.pubkey);
359
+ // For HANDSHAKE_REQ: always create or REPLACE the session with the
360
+ // new sessionId. Without this, after a Carrier session drop + re-pair
361
+ // the peer's new HANDSHAKE_REQ would be dropped because the existing
362
+ // local session has the old sessionId — so the responder never sends
363
+ // HANDSHAKE_ACK and the initiator times out forever.
364
+ if (FrameCodec.isHandshakeRequest(frame.opcode)) {
365
+ if (session && session.getSessionId() !== frame.sessionId) {
366
+ this.logger.info(`Replacing stale session for ${message.pubkey} (old=${session.getSessionId()}, new=${frame.sessionId})`);
367
+ session.close();
368
+ session = undefined;
369
+ }
370
+ if (!session) {
371
+ this.logger.info(`Auto-creating session for inbound peer ${message.pubkey} (sessionId=${frame.sessionId})`);
372
+ session = this.createSession(message.pubkey, frame.sessionId);
373
+ }
374
+ }
375
+ if (session) {
376
+ session.handleIncomingFrame(frame);
377
+ }
378
+ else {
379
+ this.logger.debug(`No session for peer ${message.pubkey}, ignoring frame opcode=0x${frame.opcode.toString(16)}`);
380
+ }
381
+ }
382
+ catch (error) {
383
+ this.logger.error(`Error processing frame from ${message.pubkey}:`, error);
384
+ }
385
+ });
386
+ // Friend requests
387
+ this.peer.onFriendRequest((request) => {
388
+ this.logger.info(`Friend request from ${request.pubkey}`);
389
+ this.emit("friend-request", request);
390
+ });
391
+ }
392
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Carrier-specific constants and re-exports
3
+ * Shared types live in src/types.ts (single source of truth)
4
+ */
5
+ export type { PeerIdentity, RemotePeer, FriendConnectionEvent, PacketFrame, } from "../types.js";
6
+ export declare const FRAME_OPCODE_HANDSHAKE_REQ = 1;
7
+ export declare const FRAME_OPCODE_HANDSHAKE_ACK = 2;
8
+ export declare const FRAME_OPCODE_DATA = 16;
9
+ export declare const FRAME_MAGIC = 170;
10
+ export declare const FRAME_HEADER_SIZE = 6;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Carrier-specific constants and re-exports
3
+ * Shared types live in src/types.ts (single source of truth)
4
+ */
5
+ // Frame protocol opcodes
6
+ export const FRAME_OPCODE_HANDSHAKE_REQ = 0x01;
7
+ export const FRAME_OPCODE_HANDSHAKE_ACK = 0x02;
8
+ export const FRAME_OPCODE_DATA = 0x10;
9
+ // Frame structure
10
+ export const FRAME_MAGIC = 0xaa;
11
+ export const FRAME_HEADER_SIZE = 6; // magic(1) + length(2) + sessionId(2) + opcode(1)