@decentnetwork/lan 0.1.44 → 0.1.46

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.
@@ -8,7 +8,13 @@ export declare class AuditLog {
8
8
  private logger;
9
9
  private buffer;
10
10
  private bufferLimit;
11
- constructor(filePath: string);
11
+ private maxBytes;
12
+ private keepFiles;
13
+ private curBytes;
14
+ constructor(filePath: string, opts?: {
15
+ maxBytes?: number;
16
+ keepFiles?: number;
17
+ });
12
18
  /**
13
19
  * Log an access attempt (allowed or denied)
14
20
  */
@@ -67,4 +73,11 @@ export declare class AuditLog {
67
73
  */
68
74
  readSince(timestamp: number): AuditEntry[];
69
75
  private write;
76
+ /**
77
+ * Size-based rotation: audit.log -> audit.log.1 -> ... -> audit.log.N,
78
+ * dropping the oldest. Bounds total on-disk audit data to
79
+ * maxBytes * (keepFiles + 1). Best-effort — a failure here must never
80
+ * break the daemon, so errors are logged and writing continues.
81
+ */
82
+ private rotate;
70
83
  }
package/dist/acl/audit.js CHANGED
@@ -2,20 +2,40 @@
2
2
  * Audit logger for ACL decisions and connection events
3
3
  * Append-only JSONL file format
4
4
  */
5
- import { appendFileSync, readFileSync, mkdirSync, existsSync } from "fs";
5
+ import { appendFileSync, readFileSync, mkdirSync, existsSync, statSync, renameSync, rmSync, } from "fs";
6
6
  import { dirname } from "path";
7
7
  import { Logger } from "../utils/logger.js";
8
+ // Defaults chosen so a busy exit (proxy_open/proxy_close per CONNECT — can
9
+ // be hundreds/sec for HLS video) can never fill the disk: at most
10
+ // maxBytes * (keepFiles + 1) on disk (~200 MB by default), oldest dropped.
11
+ const DEFAULT_MAX_BYTES = 50 * 1024 * 1024; // rotate when the live file passes 50 MB
12
+ const DEFAULT_KEEP_FILES = 3; // audit.log.1 .. audit.log.3
8
13
  export class AuditLog {
9
14
  filePath;
10
15
  logger;
11
16
  buffer = [];
12
17
  bufferLimit = 100; // Keep last N entries in memory for fast reads
13
- constructor(filePath) {
18
+ maxBytes;
19
+ keepFiles;
20
+ curBytes = 0; // running size of the live file (avoids stat() per write)
21
+ constructor(filePath, opts) {
14
22
  this.filePath = filePath;
15
23
  this.logger = new Logger({ prefix: "AuditLog" });
24
+ this.maxBytes = opts?.maxBytes ?? DEFAULT_MAX_BYTES;
25
+ this.keepFiles = opts?.keepFiles ?? DEFAULT_KEEP_FILES;
16
26
  // Ensure parent dir exists
17
27
  const dir = dirname(filePath);
18
28
  mkdirSync(dir, { recursive: true });
29
+ // Seed the byte counter from any existing file so a daemon that
30
+ // restarts onto an already-large log rotates promptly instead of
31
+ // letting it grow further.
32
+ try {
33
+ if (existsSync(this.filePath))
34
+ this.curBytes = statSync(this.filePath).size;
35
+ }
36
+ catch {
37
+ this.curBytes = 0;
38
+ }
19
39
  }
20
40
  /**
21
41
  * Log an access attempt (allowed or denied)
@@ -134,11 +154,48 @@ export class AuditLog {
134
154
  this.buffer = this.buffer.slice(-this.bufferLimit);
135
155
  }
136
156
  // Append to file (JSONL)
157
+ const line = JSON.stringify(entry) + "\n";
137
158
  try {
138
- appendFileSync(this.filePath, JSON.stringify(entry) + "\n", "utf-8");
159
+ // Rotate BEFORE the write that would push us over the cap, so the
160
+ // live file never exceeds maxBytes by more than one line.
161
+ if (this.curBytes + Buffer.byteLength(line) > this.maxBytes) {
162
+ this.rotate();
163
+ }
164
+ appendFileSync(this.filePath, line, "utf-8");
165
+ this.curBytes += Buffer.byteLength(line);
139
166
  }
140
167
  catch (error) {
141
168
  this.logger.error(`Failed to write audit entry: ${error}`);
142
169
  }
143
170
  }
171
+ /**
172
+ * Size-based rotation: audit.log -> audit.log.1 -> ... -> audit.log.N,
173
+ * dropping the oldest. Bounds total on-disk audit data to
174
+ * maxBytes * (keepFiles + 1). Best-effort — a failure here must never
175
+ * break the daemon, so errors are logged and writing continues.
176
+ */
177
+ rotate() {
178
+ try {
179
+ // Drop the oldest, then shift each backup up by one.
180
+ const oldest = `${this.filePath}.${this.keepFiles}`;
181
+ if (existsSync(oldest))
182
+ rmSync(oldest, { force: true });
183
+ for (let i = this.keepFiles - 1; i >= 1; i--) {
184
+ const src = `${this.filePath}.${i}`;
185
+ if (existsSync(src))
186
+ renameSync(src, `${this.filePath}.${i + 1}`);
187
+ }
188
+ if (existsSync(this.filePath))
189
+ renameSync(this.filePath, `${this.filePath}.1`);
190
+ this.curBytes = 0;
191
+ this.logger.info(`Rotated audit log (cap ${Math.round(this.maxBytes / 1048576)} MB, keep ${this.keepFiles})`);
192
+ }
193
+ catch (error) {
194
+ // Couldn't rotate (permissions, races). Don't let the file grow
195
+ // unbounded: reset the counter so we retry on the next write, but
196
+ // keep the daemon running.
197
+ this.logger.error(`Audit log rotation failed: ${error}`);
198
+ this.curBytes = 0;
199
+ }
200
+ }
144
201
  }
@@ -81,6 +81,34 @@ export declare function cmdFriendRequest(args: {
81
81
  configDir?: string;
82
82
  waitMs?: number;
83
83
  }): Promise<void>;
84
+ /**
85
+ * Send friend requests to MANY addresses in a single Carrier session.
86
+ *
87
+ * The expensive parts of a standalone friend-request — start, joinNetwork,
88
+ * announceSelf, and the relay-delivery wait — are PER-SESSION, not
89
+ * per-recipient. Sending N requests by calling cmdFriendRequest N times
90
+ * pays them N times (~25s each). This opens the peer once, joins/announces
91
+ * once, fires all requests, waits once, and stops once — so friending 4
92
+ * doras costs about the same as friending 1 (~25s instead of ~100s).
93
+ *
94
+ * Used by `agentnet init` to friend all federated default doras. Returns
95
+ * per-address ok/error so the caller can report which landed. If the
96
+ * daemon is already running, each request is routed via IPC (no peer).
97
+ */
98
+ export declare function cmdFriendRequestMany(args: {
99
+ targets: {
100
+ address: string;
101
+ label: string;
102
+ }[];
103
+ hello?: string;
104
+ configDir?: string;
105
+ waitMs?: number;
106
+ }): Promise<{
107
+ label: string;
108
+ address: string;
109
+ ok: boolean;
110
+ error?: string;
111
+ }[]>;
84
112
  /**
85
113
  * Accept a pending friend request.
86
114
  * Run while daemon is DOWN — opens a temporary peer, accepts, exits.
@@ -139,21 +139,27 @@ export async function cmdInit(args) {
139
139
  // them gives redundancy: any one staying up is enough to get an IP.
140
140
  const configuredUserids = new Set(config.dora?.userids ?? []);
141
141
  const dorasToFriend = DEFAULT_DORAS.filter((d) => configuredUserids.has(d.userid));
142
- for (const dora of dorasToFriend) {
143
- console.log(`\nFriending ${dora.name} (${dora.userid.slice(0, 16)}...) so the daemon can join the shared network on first start.`);
142
+ if (dorasToFriend.length > 0) {
143
+ console.log(`\nFriending ${dorasToFriend.length} default dora${dorasToFriend.length > 1 ? "s" : ""} (${dorasToFriend
144
+ .map((d) => d.name)
145
+ .join(", ")}) in one Carrier session so the daemon can join the shared network on first start.`);
144
146
  try {
145
- await cmdFriendRequest({
146
- address: dora.address,
147
+ const results = await cmdFriendRequestMany({
148
+ targets: dorasToFriend.map((d) => ({ address: d.address, label: d.name })),
147
149
  hello: `decentlan init (${nodeName})`,
148
150
  waitMs: 8000,
149
151
  configDir: dir,
150
152
  });
151
- console.log(` Friend-request dispatched. Auto-accept on the dora side will take it from here.`);
153
+ const failed = results.filter((r) => !r.ok);
154
+ for (const f of failed) {
155
+ console.warn(` ${f.label} failed: ${f.error} — re-run later: agentnet dora enable --address ${f.address}`);
156
+ }
157
+ console.log(` Friended ${results.length - failed.length}/${results.length} doras. Auto-accept on their side takes it from here (you only need one to get an IP).`);
152
158
  }
153
159
  catch (err) {
154
160
  const msg = err instanceof Error ? err.message : String(err);
155
- console.warn(` Friend-request to ${dora.name} failed: ${msg}`);
156
- console.warn(` Re-run later: agentnet dora enable --address ${dora.address}`);
161
+ console.warn(` Friend-requests failed: ${msg}`);
162
+ console.warn(` Re-run later with: agentnet up --real-tun (the daemon retries joining a dora on start).`);
157
163
  }
158
164
  }
159
165
  console.log(`\nNext: sudo agentnet service install # or 'agentnet up --real-tun' to run in foreground`);
@@ -476,6 +482,78 @@ export async function cmdFriendRequest(args) {
476
482
  console.log(`pick it up on next start (and still auto-accept). Only run 'agentnet friend-accept --pubkey ${myPubkey}'`);
477
483
  console.log(`manually if their daemon is down AND they've disabled autoAccept.`);
478
484
  }
485
+ /**
486
+ * Send friend requests to MANY addresses in a single Carrier session.
487
+ *
488
+ * The expensive parts of a standalone friend-request — start, joinNetwork,
489
+ * announceSelf, and the relay-delivery wait — are PER-SESSION, not
490
+ * per-recipient. Sending N requests by calling cmdFriendRequest N times
491
+ * pays them N times (~25s each). This opens the peer once, joins/announces
492
+ * once, fires all requests, waits once, and stops once — so friending 4
493
+ * doras costs about the same as friending 1 (~25s instead of ~100s).
494
+ *
495
+ * Used by `agentnet init` to friend all federated default doras. Returns
496
+ * per-address ok/error so the caller can report which landed. If the
497
+ * daemon is already running, each request is routed via IPC (no peer).
498
+ */
499
+ export async function cmdFriendRequestMany(args) {
500
+ const dir = args.configDir || ConfigLoader.defaultConfigDir();
501
+ const config = await ConfigLoader.load(resolve(dir, "config.yaml"));
502
+ const results = [];
503
+ // Daemon up → route each via IPC (no second peer, no announce cost).
504
+ const livePid = daemonPid(config);
505
+ if (livePid !== null) {
506
+ console.log(`Daemon is running (pid ${livePid}); routing friend-requests via IPC...`);
507
+ for (const t of args.targets) {
508
+ const res = await ipcCall(config, { op: "friend-request", address: t.address, hello: args.hello });
509
+ results.push({ label: t.label, address: t.address, ok: res.ok, error: res.ok ? undefined : res.error });
510
+ console.log(res.ok ? ` ✓ ${t.label}` : ` ✗ ${t.label}: ${res.error}`);
511
+ }
512
+ return results;
513
+ }
514
+ // Daemon down → one standalone peer session for ALL requests.
515
+ const { Peer } = await import("@decentnetwork/peer");
516
+ const keyFile = resolve(config.carrier.dataDir, "keypair.json");
517
+ console.log(`Opening peer with identity at ${keyFile}...`);
518
+ const peer = await Peer.create({
519
+ keyFile,
520
+ compatibilityMode: "legacy",
521
+ bootstrapNodes: config.carrier.bootstrapNodes,
522
+ expressNodes: config.carrier.expressNodes,
523
+ });
524
+ await peer.start();
525
+ console.log(`My address: ${peer.address()}`);
526
+ console.log(`My pubkey: ${peer.pubkey()}`);
527
+ console.log(`Joining Carrier network...`);
528
+ const joinResult = await peer.joinNetwork();
529
+ console.log(`Joined via ${joinResult.respondingNode.host}:${joinResult.respondingNode.port}`);
530
+ console.log(`Announcing self (15s)...`);
531
+ await peer.announceSelf(15000).catch((err) => {
532
+ console.warn(`Self-announce failed: ${err.message}`);
533
+ });
534
+ // Fire all requests on the one announced session.
535
+ for (const t of args.targets) {
536
+ try {
537
+ console.log(`Sending friend request to ${t.label} (${t.address.slice(0, 16)}...)...`);
538
+ await peer.sendFriendRequest(t.address, args.hello || "Decent AgentNet friend request");
539
+ results.push({ label: t.label, address: t.address, ok: true });
540
+ }
541
+ catch (err) {
542
+ const msg = err instanceof Error ? err.message : String(err);
543
+ console.warn(` Failed to send to ${t.label}: ${msg}`);
544
+ results.push({ label: t.label, address: t.address, ok: false, error: msg });
545
+ }
546
+ }
547
+ // Single shared wait for relay delivery of all requests above.
548
+ const waitMs = args.waitMs ?? 8000;
549
+ console.log(`Waiting ${waitMs}ms for relay delivery...`);
550
+ await new Promise((r) => setTimeout(r, waitMs));
551
+ const myUserid = peer.userid();
552
+ await peer.stop();
553
+ console.log(`\nFriend requests sent. Recipients running with autoAccept (the default) have already`);
554
+ console.log(`accepted — they can confirm with 'agentnet diag' and look for userid ${myUserid}.`);
555
+ return results;
556
+ }
479
557
  /**
480
558
  * Accept a pending friend request.
481
559
  * Run while daemon is DOWN — opens a temporary peer, accepts, exits.
@@ -551,12 +551,19 @@ export class DaemonServer {
551
551
  allowHosts: this.config.proxy.allowHosts ?? [],
552
552
  allowConnectPorts: this.config.proxy.allowConnectPorts,
553
553
  resolvePeerName: (srcIp) => this.ipam.resolveIp(srcIp)?.name,
554
- onTunnelOpen: ({ src, srcName, target }) => {
555
- this.auditLog.logProxyOpen({ srcIp: src, srcName, target });
556
- },
557
- onTunnelClose: ({ src, srcName, target, bytesTransferred }) => {
558
- this.auditLog.logProxyClose({ srcIp: src, srcName, target, bytesTransferred });
559
- },
554
+ // Per-tunnel audit is opt-in: on a busy exit it floods the
555
+ // log (the 13 GB audit.log bug). Off by default; ACL and
556
+ // connect events are still always audited.
557
+ onTunnelOpen: this.config.proxy.auditTunnels
558
+ ? ({ src, srcName, target }) => {
559
+ this.auditLog.logProxyOpen({ srcIp: src, srcName, target });
560
+ }
561
+ : undefined,
562
+ onTunnelClose: this.config.proxy.auditTunnels
563
+ ? ({ src, srcName, target, bytesTransferred }) => {
564
+ this.auditLog.logProxyClose({ srcIp: src, srcName, target, bytesTransferred });
565
+ }
566
+ : undefined,
560
567
  });
561
568
  await this.connectProxy.start();
562
569
  this.logger.info(`Proxy listening on ${tunIp}:${this.config.proxy.port}`);
package/dist/types.d.ts CHANGED
@@ -129,6 +129,7 @@ export interface ProxyConfig {
129
129
  port: number;
130
130
  allowHosts?: string[];
131
131
  allowConnectPorts?: number[];
132
+ auditTunnels?: boolean;
132
133
  }
133
134
  export interface FriendsConfig {
134
135
  /** When true (default), the running daemon auto-accepts incoming
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/lan",
3
- "version": "0.1.44",
3
+ "version": "0.1.46",
4
4
  "description": "Private virtual LAN for self-hosted services and AI agents, built on Elastos Carrier. NAT-traversal, name service, ACL, all over a peer-to-peer mesh — no public IP required.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",