pyre-cli 2.0.0 → 3.0.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.
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import fs2 from "fs";
4
+ import fs4 from "fs";
5
5
  import readline2 from "readline";
6
6
  import { Command } from "commander";
7
- import chalk8 from "chalk";
7
+ import chalk9 from "chalk";
8
8
 
9
9
  // src/monitors/collectors.ts
10
10
  import chalk from "chalk";
@@ -511,7 +511,7 @@ async function collectPackets() {
511
511
  let topProcesses = [];
512
512
  let allProcesses = [];
513
513
  try {
514
- const lsof = (await run("lsof -i -n -P -r 1 2>/dev/null | tail -n +2 | awk '{print $1, $2}' | sort | uniq -c | sort -rn", "")).trim();
514
+ const lsof = (await run("lsof -i -n -P 2>/dev/null | tail -n +2 | awk '{print $1, $2}' | sort | uniq -c | sort -rn", "")).trim();
515
515
  const lsofLines = lsof.split("\n");
516
516
  topProcesses = lsofLines.map((line) => {
517
517
  const parts = line.trim().split(/\s+/);
@@ -2176,42 +2176,67 @@ async function showSplash() {
2176
2176
 
2177
2177
  // src/p2p/server.ts
2178
2178
  import net from "net";
2179
+ import tls from "tls";
2180
+ import fs2 from "fs";
2181
+ import crypto2 from "crypto";
2179
2182
  import chalk7 from "chalk";
2180
2183
 
2181
2184
  // src/p2p/protocol.ts
2185
+ import crypto from "crypto";
2182
2186
  var HEADER_SIZE = 4;
2183
2187
  var MAX_MESSAGE_SIZE = 16 * 1024 * 1024;
2184
- function buildMessage(kind, payload) {
2188
+ var HMAC_ALGO = "sha256";
2189
+ function buildMessage(kind, payload, hmac) {
2185
2190
  const envelope = { kind, payload };
2191
+ if (hmac) envelope.hmac = hmac;
2186
2192
  const body = Buffer.from(JSON.stringify(envelope), "utf-8");
2187
2193
  const header2 = Buffer.alloc(HEADER_SIZE);
2188
2194
  header2.writeUInt32BE(body.length, 0);
2189
2195
  return Buffer.concat([header2, body]);
2190
2196
  }
2191
- function encodeAuth(password) {
2192
- return buildMessage("auth", { password });
2197
+ function generateNonce() {
2198
+ return crypto.randomBytes(32).toString("hex");
2193
2199
  }
2194
- function encodeAuthOk() {
2195
- return buildMessage("auth-ok", { ok: true });
2200
+ function hashPassword(password, nonce) {
2201
+ return crypto.createHash("sha256").update(`${password}:${nonce}`).digest("hex");
2196
2202
  }
2197
- function encodeAuthFail(reason) {
2198
- return buildMessage("auth-fail", { ok: false, reason });
2203
+ function computeHMAC(body, key) {
2204
+ if (!key) return void 0;
2205
+ return crypto.createHmac(HMAC_ALGO, key).update(body).digest("hex");
2199
2206
  }
2200
- function encodeData(stats) {
2201
- return buildMessage("data", { stats });
2202
- }
2203
- function encodePing() {
2204
- return buildMessage("ping", { ts: Date.now() });
2205
- }
2206
- function encodePong(ts) {
2207
- return buildMessage("pong", { ts });
2207
+ function verifyHMAC(body, expectedHmac, key) {
2208
+ if (!key) return expectedHmac === void 0;
2209
+ if (!expectedHmac) return false;
2210
+ try {
2211
+ const parsed = JSON.parse(body);
2212
+ const payloadWithoutHmac = { ...parsed, hmac: void 0 };
2213
+ const canonical = JSON.stringify(payloadWithoutHmac);
2214
+ const computed = crypto.createHmac(HMAC_ALGO, key).update(canonical).digest("hex");
2215
+ return crypto.timingSafeEqual(Buffer.from(computed), Buffer.from(expectedHmac));
2216
+ } catch {
2217
+ return false;
2218
+ }
2208
2219
  }
2209
2220
  function encodeDisconnect(reason) {
2210
2221
  return buildMessage("disconnect", { reason });
2211
2222
  }
2223
+ function decodeMessage(buf) {
2224
+ const str = buf.toString("utf-8");
2225
+ const msg = JSON.parse(str);
2226
+ return msg;
2227
+ }
2228
+ function signMessage(kind, payload, key) {
2229
+ const envelope = { kind, payload };
2230
+ const bodyJson = JSON.stringify(envelope);
2231
+ const hmac = computeHMAC(bodyJson, key);
2232
+ return buildMessage(kind, payload, hmac);
2233
+ }
2212
2234
 
2213
2235
  // src/p2p/server.ts
2214
2236
  var HEADER_SIZE2 = 4;
2237
+ var DEFAULT_RATE_LIMIT_MAX = 5;
2238
+ var DEFAULT_RATE_LIMIT_WINDOW = 6e4;
2239
+ var HMAC_KEY_LENGTH = 32;
2215
2240
  function readMessageFromPeer(peer) {
2216
2241
  while (peer.readBuffer.length > 0) {
2217
2242
  if (peer.readExpectingHeader) {
@@ -2236,13 +2261,36 @@ function readMessageFromPeer(peer) {
2236
2261
  }
2237
2262
  return null;
2238
2263
  }
2264
+ function isIPAllowed(ip, allowedIPs, deniedIPs) {
2265
+ for (const denied of deniedIPs) {
2266
+ if (ip === denied || ip.startsWith(denied)) {
2267
+ return false;
2268
+ }
2269
+ }
2270
+ if (allowedIPs.length === 0) {
2271
+ return true;
2272
+ }
2273
+ for (const allowed of allowedIPs) {
2274
+ if (ip === allowed || ip.startsWith(allowed)) {
2275
+ return true;
2276
+ }
2277
+ }
2278
+ return false;
2279
+ }
2280
+ function deriveHMACKey(password) {
2281
+ return crypto2.createHash("sha256").update(password).digest("hex").slice(0, HMAC_KEY_LENGTH);
2282
+ }
2239
2283
  var P2PServer = class {
2240
2284
  server = null;
2241
2285
  peers = /* @__PURE__ */ new Map();
2242
2286
  options;
2243
2287
  running = false;
2288
+ rateLimitMap = /* @__PURE__ */ new Map();
2289
+ auditStream = null;
2290
+ hmacKey;
2244
2291
  constructor(options) {
2245
2292
  this.options = options;
2293
+ this.hmacKey = deriveHMACKey(options.password);
2246
2294
  }
2247
2295
  get info() {
2248
2296
  return {
@@ -2252,16 +2300,42 @@ var P2PServer = class {
2252
2300
  };
2253
2301
  }
2254
2302
  async start() {
2303
+ if (this.options.auditLog) {
2304
+ try {
2305
+ fs2.mkdirSync(this.options.auditLog, { recursive: true });
2306
+ const logFile = `${this.options.auditLog}/p2p-audit.log`;
2307
+ this.auditStream = fs2.createWriteStream(logFile, { flags: "a" });
2308
+ } catch {
2309
+ console.log(chalk7.yellow("Warning: could not open audit log"));
2310
+ }
2311
+ }
2255
2312
  return new Promise((resolve, reject) => {
2256
- this.server = net.createServer((socket) => this.onConnection(socket));
2313
+ const useTLS = !!(this.options.tlsCert && this.options.tlsKey);
2314
+ if (useTLS) {
2315
+ const tlsOptions = {
2316
+ key: fs2.readFileSync(this.options.tlsKey),
2317
+ cert: fs2.readFileSync(this.options.tlsCert)
2318
+ };
2319
+ if (this.options.tlsCA) {
2320
+ tlsOptions.ca = fs2.readFileSync(this.options.tlsCA);
2321
+ }
2322
+ this.server = tls.createServer(tlsOptions, (socket) => this.onConnection(socket));
2323
+ } else {
2324
+ this.server = net.createServer((socket) => this.onConnection(socket));
2325
+ }
2257
2326
  this.server.on("error", (err) => reject(err));
2258
2327
  this.server.listen(this.options.port, this.options.host, () => {
2259
2328
  this.running = true;
2260
2329
  const addr = this.server?.address();
2261
2330
  const bound = addr ? typeof addr === "object" ? `${addr.address}:${addr.port}` : String(addr) : "unknown";
2262
- console.log(chalk7.green(`P2P server listening on ${bound}`));
2331
+ const protocol = useTLS ? "TLS" : "TCP";
2332
+ console.log(chalk7.green(`P2P server listening on ${bound} (${protocol})`));
2263
2333
  console.log(chalk7.dim(` Password: ${this.options.password}`));
2264
2334
  console.log(chalk7.dim(` Interval: ${this.options.intervalMs}ms`));
2335
+ const allowedIPs = this.options.allowedIPs ?? [];
2336
+ const deniedIPs = this.options.deniedIPs ?? [];
2337
+ if (allowedIPs.length > 0) console.log(chalk7.dim(` Allowed IPs: ${allowedIPs.join(", ")}`));
2338
+ if (deniedIPs.length > 0) console.log(chalk7.dim(` Denied IPs: ${deniedIPs.join(", ")}`));
2265
2339
  resolve();
2266
2340
  });
2267
2341
  });
@@ -2272,12 +2346,68 @@ var P2PServer = class {
2272
2346
  this.disconnectPeer(peer, "Server shutting down");
2273
2347
  }
2274
2348
  this.peers.clear();
2349
+ this.rateLimitMap.clear();
2275
2350
  if (this.server) {
2276
2351
  this.server.close();
2277
2352
  this.server = null;
2278
2353
  }
2354
+ if (this.auditStream) {
2355
+ this.auditStream.end();
2356
+ this.auditStream = null;
2357
+ }
2358
+ }
2359
+ audit(ip, event, detail) {
2360
+ if (!this.auditStream) return;
2361
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
2362
+ this.auditStream.write(`${ts} ${ip} ${event} ${detail}
2363
+ `);
2364
+ }
2365
+ getPeerIP(socket) {
2366
+ const addr = socket.remoteAddress;
2367
+ if (!addr) return "unknown";
2368
+ if (addr.startsWith("::ffff:")) {
2369
+ return addr.slice(7);
2370
+ }
2371
+ return addr;
2372
+ }
2373
+ checkRateLimit(ip) {
2374
+ const maxAttempts = this.options.rateLimitMaxAttempts ?? DEFAULT_RATE_LIMIT_MAX;
2375
+ const windowMs = this.options.rateLimitWindowMs ?? DEFAULT_RATE_LIMIT_WINDOW;
2376
+ const now = Date.now();
2377
+ const entry = this.rateLimitMap.get(ip);
2378
+ if (!entry) {
2379
+ this.rateLimitMap.set(ip, { attempts: 1, firstAttempt: now });
2380
+ return true;
2381
+ }
2382
+ if (now - entry.firstAttempt > windowMs) {
2383
+ this.rateLimitMap.set(ip, { attempts: 1, firstAttempt: now });
2384
+ return true;
2385
+ }
2386
+ entry.attempts++;
2387
+ if (entry.attempts > maxAttempts) {
2388
+ return false;
2389
+ }
2390
+ return true;
2391
+ }
2392
+ cleanupRateLimit(ip) {
2393
+ this.rateLimitMap.delete(ip);
2279
2394
  }
2280
2395
  onConnection(socket) {
2396
+ const ip = this.getPeerIP(socket);
2397
+ const allowedIPs = this.options.allowedIPs ?? [];
2398
+ const deniedIPs = this.options.deniedIPs ?? [];
2399
+ if (!isIPAllowed(ip, allowedIPs, deniedIPs)) {
2400
+ this.audit(ip, "BLOCKED", "IP not allowed");
2401
+ console.log(chalk7.yellow(`Blocked IP: ${ip}`));
2402
+ socket.destroy();
2403
+ return;
2404
+ }
2405
+ if (!this.checkRateLimit(ip)) {
2406
+ this.audit(ip, "RATE_LIMITED", "Too many auth attempts");
2407
+ console.log(chalk7.yellow(`Rate limit exceeded for IP: ${ip}`));
2408
+ socket.destroy();
2409
+ return;
2410
+ }
2281
2411
  const peer = {
2282
2412
  socket,
2283
2413
  authenticated: false,
@@ -2285,11 +2415,14 @@ var P2PServer = class {
2285
2415
  pingHandle: null,
2286
2416
  readBuffer: Buffer.alloc(0),
2287
2417
  readExpectingHeader: true,
2288
- readMessageLength: 0
2418
+ readMessageLength: 0,
2419
+ ip,
2420
+ pendingNonce: null
2289
2421
  };
2290
2422
  this.peers.set(socket, peer);
2291
2423
  const remoteAddr = socket.remoteAddress ? `${socket.remoteAddress}:${socket.remotePort}` : "unknown";
2292
2424
  console.log(chalk7.cyan(`Peer connected: ${remoteAddr}`));
2425
+ this.audit(ip, "CONNECT", remoteAddr);
2293
2426
  socket.on("data", (data) => this.onData(peer, data));
2294
2427
  socket.on("close", () => this.onPeerClose(peer));
2295
2428
  socket.on("error", (err) => this.onPeerError(peer, err));
@@ -2299,6 +2432,9 @@ var P2PServer = class {
2299
2432
  this.disconnectPeer(peer, "Authentication timeout");
2300
2433
  }
2301
2434
  });
2435
+ const nonce = generateNonce();
2436
+ this.sendSigned(peer, signMessage("challenge", { nonce }, this.hmacKey));
2437
+ peer.pendingNonce = nonce;
2302
2438
  }
2303
2439
  onData(peer, data) {
2304
2440
  peer.readBuffer = Buffer.concat([peer.readBuffer, data]);
@@ -2312,33 +2448,56 @@ var P2PServer = class {
2312
2448
  }
2313
2449
  handleAuthMessage(peer, body) {
2314
2450
  try {
2315
- const msg = JSON.parse(body.toString("utf-8"));
2316
- if (msg.kind === "auth" && typeof msg.payload?.password === "string") {
2317
- if (msg.payload.password === this.options.password) {
2318
- peer.authenticated = true;
2319
- peer.socket.write(encodeAuthOk());
2320
- console.log(chalk7.green(`Peer authenticated: ${peer.socket.remoteAddress}`));
2321
- this.startDataStream(peer);
2322
- this.startPing(peer);
2323
- } else {
2324
- peer.socket.write(encodeAuthFail("Invalid password"));
2325
- this.disconnectPeer(peer, "Authentication failed");
2326
- }
2327
- } else {
2328
- peer.socket.write(encodeAuthFail("Expected auth message first"));
2451
+ const raw = body.toString("utf-8");
2452
+ const msg = decodeMessage(body);
2453
+ if (msg.kind !== "auth") {
2454
+ peer.socket.write(signMessage("auth-fail", { ok: false, reason: "Expected auth message first" }, this.hmacKey));
2329
2455
  this.disconnectPeer(peer, "Expected auth message");
2456
+ return;
2457
+ }
2458
+ const authPayload = msg.payload;
2459
+ if (!authPayload.passwordHash || !authPayload.nonce) {
2460
+ peer.socket.write(signMessage("auth-fail", { ok: false, reason: "Invalid auth payload" }, this.hmacKey));
2461
+ this.disconnectPeer(peer, "Invalid auth payload");
2462
+ return;
2463
+ }
2464
+ if (peer.pendingNonce && authPayload.nonce !== peer.pendingNonce) {
2465
+ this.audit(peer.ip, "AUTH_FAIL", "Nonce mismatch");
2466
+ peer.socket.write(signMessage("auth-fail", { ok: false, reason: "Invalid nonce" }, this.hmacKey));
2467
+ this.disconnectPeer(peer, "Invalid nonce");
2468
+ return;
2330
2469
  }
2470
+ const expectedHash = hashPassword(this.options.password, authPayload.nonce);
2471
+ if (authPayload.passwordHash !== expectedHash) {
2472
+ this.audit(peer.ip, "AUTH_FAIL", "Invalid password hash");
2473
+ peer.socket.write(signMessage("auth-fail", { ok: false, reason: "Invalid password" }, this.hmacKey));
2474
+ this.disconnectPeer(peer, "Authentication failed");
2475
+ return;
2476
+ }
2477
+ peer.authenticated = true;
2478
+ this.cleanupRateLimit(peer.ip);
2479
+ peer.pendingNonce = null;
2480
+ peer.socket.write(signMessage("auth-ok", { ok: true }, this.hmacKey));
2481
+ console.log(chalk7.green(`Peer authenticated: ${peer.socket.remoteAddress}`));
2482
+ this.audit(peer.ip, "AUTH_OK", "Challenge-response successful");
2483
+ this.startDataStream(peer);
2484
+ this.startPing(peer);
2331
2485
  } catch {
2332
- peer.socket.write(encodeAuthFail("Invalid message format"));
2486
+ peer.socket.write(signMessage("auth-fail", { ok: false, reason: "Invalid message format" }, this.hmacKey));
2333
2487
  this.disconnectPeer(peer, "Invalid message format");
2334
2488
  }
2335
2489
  }
2490
+ sendSigned(peer, message) {
2491
+ if (!peer.socket.destroyed) {
2492
+ peer.socket.write(message);
2493
+ }
2494
+ }
2336
2495
  startDataStream(peer) {
2337
2496
  const send = async () => {
2338
2497
  if (!peer.authenticated || peer.socket.destroyed) return;
2339
2498
  try {
2340
2499
  const data = await collectAll({ detailed: this.options.detailed });
2341
- peer.socket.write(encodeData(data));
2500
+ this.sendSigned(peer, signMessage("data", { stats: data }, this.hmacKey));
2342
2501
  } catch {
2343
2502
  }
2344
2503
  };
@@ -2348,25 +2507,28 @@ var P2PServer = class {
2348
2507
  startPing(peer) {
2349
2508
  peer.pingHandle = setInterval(() => {
2350
2509
  if (peer.socket.destroyed) return;
2351
- peer.socket.write(encodePing());
2510
+ this.sendSigned(peer, signMessage("ping", { ts: Date.now() }, this.hmacKey));
2352
2511
  }, 5e3);
2353
2512
  }
2354
2513
  onPeerClose(peer) {
2355
2514
  this.cleanupPeer(peer);
2356
2515
  console.log(chalk7.yellow(`Peer disconnected: ${peer.socket.remoteAddress}`));
2516
+ this.audit(peer.ip, "DISCONNECT", "Peer closed connection");
2357
2517
  }
2358
2518
  onPeerError(peer, err) {
2359
2519
  const code = err.code;
2360
2520
  if (code === "ECONNRESET") {
2361
2521
  console.log(chalk7.yellow(`Peer connection reset: ${peer.socket.remoteAddress}`));
2522
+ this.audit(peer.ip, "ERROR", "Connection reset");
2362
2523
  } else {
2363
2524
  console.log(chalk7.red(`Peer error: ${err.message}`));
2525
+ this.audit(peer.ip, "ERROR", err.message);
2364
2526
  }
2365
2527
  this.cleanupPeer(peer);
2366
2528
  }
2367
2529
  disconnectPeer(peer, reason) {
2368
2530
  try {
2369
- peer.socket.write(encodeDisconnect(reason));
2531
+ this.sendSigned(peer, signMessage("disconnect", { reason }, this.hmacKey));
2370
2532
  } catch {
2371
2533
  }
2372
2534
  peer.socket.destroy();
@@ -2386,6 +2548,10 @@ var P2PServer = class {
2386
2548
 
2387
2549
  // src/p2p/client.ts
2388
2550
  import net2 from "net";
2551
+ import tls2 from "tls";
2552
+ import fs3 from "fs";
2553
+ import crypto3 from "crypto";
2554
+ import chalk8 from "chalk";
2389
2555
  var P2PClient = class {
2390
2556
  socket = null;
2391
2557
  options;
@@ -2395,8 +2561,12 @@ var P2PClient = class {
2395
2561
  onStatusCallback = null;
2396
2562
  reconnectTimer = null;
2397
2563
  pingHandle = null;
2564
+ pendingNonce = null;
2565
+ auditStream = null;
2566
+ hmacKey;
2398
2567
  constructor(options) {
2399
2568
  this.options = options;
2569
+ this.hmacKey = deriveHMACKey2(options.password);
2400
2570
  }
2401
2571
  setOnData(cb) {
2402
2572
  this.onDataCallback = cb;
@@ -2405,23 +2575,47 @@ var P2PClient = class {
2405
2575
  this.onStatusCallback = cb;
2406
2576
  }
2407
2577
  async connect() {
2578
+ if (this.options.auditLog) {
2579
+ try {
2580
+ fs3.mkdirSync(this.options.auditLog, { recursive: true });
2581
+ const logFile = `${this.options.auditLog}/p2p-client-audit.log`;
2582
+ this.auditStream = fs3.createWriteStream(logFile, { flags: "a" });
2583
+ } catch {
2584
+ console.log(chalk8.yellow("Warning: could not open audit log"));
2585
+ }
2586
+ }
2408
2587
  return new Promise((resolve, reject) => {
2409
- this.socket = new net2.Socket();
2588
+ const useTLS = !!this.options.tls;
2589
+ if (useTLS) {
2590
+ const tlsOptions = {
2591
+ host: this.options.host,
2592
+ port: this.options.port,
2593
+ rejectUnauthorized: !this.options.tlsInsecure
2594
+ };
2595
+ if (this.options.tlsCA) {
2596
+ tlsOptions.ca = fs3.readFileSync(this.options.tlsCA);
2597
+ }
2598
+ this.socket = tls2.connect(tlsOptions, () => {
2599
+ this.status("Connecting...");
2600
+ });
2601
+ } else {
2602
+ this.socket = new net2.Socket();
2603
+ this.socket.on("data", (data) => this.onData(data));
2604
+ this.socket.on("close", () => this.onClose());
2605
+ this.socket.on("error", (err) => this.onError(err));
2606
+ this.socket.on("timeout", () => this.onTimeout());
2607
+ this.socket.setTimeout(1e4);
2608
+ this.socket.connect(this.options.port, this.options.host, () => {
2609
+ this.status("Connecting...");
2610
+ });
2611
+ this.socket.once("error", (err) => {
2612
+ reject(err);
2613
+ });
2614
+ }
2410
2615
  this.socket.on("data", (data) => this.onData(data));
2411
2616
  this.socket.on("close", () => this.onClose());
2412
2617
  this.socket.on("error", (err) => this.onError(err));
2413
2618
  this.socket.on("timeout", () => this.onTimeout());
2414
- this.socket.setTimeout(1e4);
2415
- this.socket.connect(this.options.port, this.options.host, () => {
2416
- this.running = true;
2417
- this.socket?.setTimeout(0);
2418
- this.socket?.write(encodeAuth(this.options.password));
2419
- this.status("Connecting...");
2420
- resolve();
2421
- });
2422
- this.socket.once("error", (err) => {
2423
- reject(err);
2424
- });
2425
2619
  });
2426
2620
  }
2427
2621
  disconnect() {
@@ -2436,12 +2630,16 @@ var P2PClient = class {
2436
2630
  }
2437
2631
  if (this.socket) {
2438
2632
  try {
2439
- this.socket.write(encodeDisconnect("Client closing"));
2633
+ this.sendSigned(encodeDisconnect("Client closing"));
2440
2634
  } catch {
2441
2635
  }
2442
2636
  this.socket.destroy();
2443
2637
  this.socket = null;
2444
2638
  }
2639
+ if (this.auditStream) {
2640
+ this.auditStream.end();
2641
+ this.auditStream = null;
2642
+ }
2445
2643
  this.status("Disconnected");
2446
2644
  }
2447
2645
  onData(data) {
@@ -2470,31 +2668,50 @@ var P2PClient = class {
2470
2668
  }
2471
2669
  handleMessage(body) {
2472
2670
  try {
2473
- const msg = JSON.parse(body.toString("utf-8"));
2671
+ const raw = body.toString("utf-8");
2672
+ const msg = decodeMessage(body);
2673
+ if (!verifyHMAC(raw, msg.hmac ?? "", this.hmacKey)) {
2674
+ this.status("Message integrity check failed");
2675
+ return;
2676
+ }
2474
2677
  switch (msg.kind) {
2678
+ case "challenge":
2679
+ const challengePayload = msg.payload;
2680
+ if (challengePayload.nonce) {
2681
+ this.pendingNonce = challengePayload.nonce;
2682
+ const passwordHash = hashPassword(this.options.password, challengePayload.nonce);
2683
+ this.sendSigned(signMessage("auth", { passwordHash, nonce: challengePayload.nonce }, this.hmacKey));
2684
+ this.status("Authenticating...");
2685
+ }
2686
+ break;
2475
2687
  case "auth-ok":
2476
2688
  this.status("Authenticated \u2014 receiving live data");
2477
2689
  this.startPing();
2478
2690
  break;
2479
2691
  case "auth-fail":
2480
- this.status(`Authentication failed: ${msg.payload.reason}`);
2692
+ const failPayload = msg.payload;
2693
+ this.status(`Authentication failed: ${failPayload.reason || "unknown"}`);
2481
2694
  this.disconnect();
2482
2695
  break;
2483
2696
  case "data":
2484
- if (this.onDataCallback && msg.payload?.stats) {
2485
- this.onDataCallback(msg.payload.stats);
2697
+ if (this.onDataCallback) {
2698
+ const dataPayload = msg.payload;
2699
+ if (dataPayload.stats) {
2700
+ this.onDataCallback(dataPayload.stats);
2701
+ }
2486
2702
  }
2487
2703
  break;
2488
2704
  case "ping":
2489
2705
  if (this.socket && !this.socket.destroyed) {
2490
2706
  const ts = msg.payload.ts;
2491
- this.socket.write(encodePong(ts));
2707
+ this.sendSigned(signMessage("pong", { ts }, this.hmacKey));
2492
2708
  }
2493
2709
  break;
2494
2710
  case "pong":
2495
2711
  break;
2496
2712
  case "disconnect":
2497
- this.status(`Server disconnected: ${msg.payload.reason}`);
2713
+ const disconnectPayload = msg.payload;
2714
+ this.status(`Server disconnected: ${disconnectPayload.reason || "unknown"}`);
2498
2715
  this.disconnect();
2499
2716
  break;
2500
2717
  default:
@@ -2503,10 +2720,15 @@ var P2PClient = class {
2503
2720
  } catch {
2504
2721
  }
2505
2722
  }
2723
+ sendSigned(message) {
2724
+ if (this.socket && !this.socket.destroyed) {
2725
+ this.socket.write(message);
2726
+ }
2727
+ }
2506
2728
  startPing() {
2507
2729
  this.pingHandle = setInterval(() => {
2508
2730
  if (this.socket && !this.socket.destroyed) {
2509
- this.socket.write(encodePing());
2731
+ this.sendSigned(signMessage("ping", { ts: Date.now() }, this.hmacKey));
2510
2732
  }
2511
2733
  }, 15e3);
2512
2734
  }
@@ -2525,6 +2747,8 @@ var P2PClient = class {
2525
2747
  this.status("Connection refused \u2014 is the server running?");
2526
2748
  } else if (code === "ETIMEDOUT") {
2527
2749
  this.status("Connection timed out");
2750
+ } else if (code === "UNABLE_TO_VERIFY_LEAF_SIGNATURE") {
2751
+ this.status("TLS certificate verification failed \u2014 use --p2p-insecure to skip");
2528
2752
  } else {
2529
2753
  this.status(`Connection error: ${err.message}`);
2530
2754
  }
@@ -2538,11 +2762,21 @@ var P2PClient = class {
2538
2762
  this.onStatusCallback(msg);
2539
2763
  }
2540
2764
  }
2765
+ audit(event, detail) {
2766
+ if (!this.auditStream) return;
2767
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
2768
+ const ip = this.options.host;
2769
+ this.auditStream.write(`${ts} ${ip} ${event} ${detail}
2770
+ `);
2771
+ }
2541
2772
  };
2773
+ function deriveHMACKey2(password) {
2774
+ return crypto3.createHash("sha256").update(password).digest("hex").slice(0, 32);
2775
+ }
2542
2776
 
2543
2777
  // src/index.ts
2544
2778
  var program = new Command();
2545
- program.name("pyre").version("2.0.0").description("Mac system monitoring CLI: interactive live dashboard, stats, graphs, export, packet monitor, battery predictor").option("-j, --json", "Output as JSON").option("-c, --csv", "Output as CSV").option("-t, --tsv", "Output as TSV").option("--detailed", "Include detailed system info and sensor readings").option("--interval <seconds>", "Refresh interval for live mode", "2").option("--once", "Show a single static snapshot instead of live feed").option("--out <file>", "Also write the snapshot output to a file (--once/--json/--csv/--tsv modes)").option("--export-dir <dir>", "Directory used for live-mode snapshot exports and logs", "./pyre-exports").option("--log", "Start continuous CSV logging immediately when live mode starts").option("--tree", "Show process tree view instead of flat list").option("--sort <key>", "Sort processes by: cpu, mem, pid, user, command, state, threads, runtime", "cpu").option("--packets", "Include packet monitor panel in output").option("--limit <n>", "Max number of processes to include in --once/--json/--csv/--tsv snapshots (0 = all)", "10").option("--p2p-host <host>", "P2P host address (server: bind address, client: server address)", "0.0.0.0").option("--p2p-port <port>", "P2P port number", "9876").option("--p2p-password <password>", "Password for P2P authentication");
2779
+ program.name("pyre").version("2.0.0").description("Mac system monitoring CLI: interactive live dashboard, stats, graphs, export, packet monitor, battery predictor").option("-j, --json", "Output as JSON").option("-c, --csv", "Output as CSV").option("-t, --tsv", "Output as TSV").option("--detailed", "Include detailed system info and sensor readings").option("--interval <seconds>", "Refresh interval for live mode", "2").option("--once", "Show a single static snapshot instead of live feed").option("--out <file>", "Also write the snapshot output to a file (--once/--json/--csv/--tsv modes)").option("--export-dir <dir>", "Directory used for live-mode snapshot exports and logs", "./pyre-exports").option("--log", "Start continuous CSV logging immediately when live mode starts").option("--tree", "Show process tree view instead of flat list").option("--sort <key>", "Sort processes by: cpu, mem, pid, user, command, state, threads, runtime", "cpu").option("--packets", "Include packet monitor panel in output").option("--limit <n>", "Max number of processes to include in --once/--json/--csv/--tsv snapshots (0 = all)", "10").option("--p2p-host <host>", "P2P host address (server: bind address, client: server address)", "0.0.0.0").option("--p2p-port <port>", "P2P port number", "9876").option("--p2p-password <password>", "Password for P2P authentication").option("--p2p-tls", "Enable TLS encryption for P2P connections").option("--p2p-cert <file>", "TLS certificate file (PEM) for P2P server").option("--p2p-key <file>", "TLS private key file (PEM) for P2P server").option("--p2p-ca <file>", "TLS CA certificate file (PEM) for P2P client").option("--p2p-insecure", "Skip TLS certificate verification (client only)").option("--p2p-rate-limit <n>", "Max auth attempts per IP per minute (default: 5)", "5").option("--p2p-allow <ips>", "Comma-separated list of allowed IPs (empty = all)").option("--p2p-deny <ips>", "Comma-separated list of denied IPs").option("--p2p-audit-log <dir>", "Directory for P2P audit logs").option("--p2p-hmac-key <key>", "HMAC key for message signing (default: derived from password)");
2546
2780
  program.parse(process.argv);
2547
2781
  var opts = program.opts();
2548
2782
  function isExportMode() {
@@ -2576,7 +2810,7 @@ async function main() {
2576
2810
  await runP2PConnect();
2577
2811
  return;
2578
2812
  } else {
2579
- console.log(chalk8.red("Usage: pyre p2p <server|connect> [options]"));
2813
+ console.log(chalk9.red("Usage: pyre p2p <server|connect> [options]"));
2580
2814
  process.exit(1);
2581
2815
  }
2582
2816
  }
@@ -2590,13 +2824,13 @@ async function main() {
2590
2824
  else output = formatTable(data, { width: process.stdout.columns || 80, sortBy: opts.sort, treeView: opts.tree, visible: { packets: opts.packets ? true : void 0 } });
2591
2825
  console.log(output);
2592
2826
  if (opts.out) {
2593
- fs2.writeFileSync(opts.out, stripAnsi(output));
2594
- console.log(chalk8.dim(`
2827
+ fs4.writeFileSync(opts.out, stripAnsi(output));
2828
+ console.log(chalk9.dim(`
2595
2829
  Saved to ${opts.out}`));
2596
2830
  }
2597
2831
  }
2598
2832
  main().catch((err) => {
2599
- console.error(chalk8.red(`Error: ${err.message}`));
2833
+ console.error(chalk9.red(`Error: ${err.message}`));
2600
2834
  process.exit(1);
2601
2835
  });
2602
2836
  async function runP2PServer() {
@@ -2605,19 +2839,53 @@ async function runP2PServer() {
2605
2839
  const password = opts.p2pPassword || "";
2606
2840
  const interval = parseFloat(opts.interval) || 2;
2607
2841
  const detailed = !!opts.detailed;
2842
+ const tlsCert = opts.p2pCert;
2843
+ const tlsKey = opts.p2pKey;
2844
+ const tlsCA = opts.p2pCA;
2845
+ const rateLimit = parseInt(opts.p2pRateLimit, 10) || 5;
2846
+ const allowList = opts.p2pAllow ? opts.p2pAllow.split(",").map((s) => s.trim()).filter(Boolean) : [];
2847
+ const denyList = opts.p2pDeny ? opts.p2pDeny.split(",").map((s) => s.trim()).filter(Boolean) : [];
2608
2848
  if (!password) {
2609
- console.log(chalk8.red("Error: a password is required for P2P server mode. Use --p2p-password <password>"));
2849
+ console.log(chalk9.red("Error: a password is required for P2P server mode. Use --p2p-password <password>"));
2850
+ process.exit(1);
2851
+ }
2852
+ if (tlsCert && !tlsKey) {
2853
+ console.log(chalk9.red("Error: --p2p-key is required when --p2p-cert is specified"));
2854
+ process.exit(1);
2855
+ }
2856
+ if (tlsKey && !tlsCert) {
2857
+ console.log(chalk9.red("Error: --p2p-cert is required when --p2p-key is specified"));
2610
2858
  process.exit(1);
2611
2859
  }
2612
- const server = new P2PServer({ host, port, password, intervalMs: interval * 1e3, detailed });
2613
- console.log(chalk8.bold(`
2860
+ const server = new P2PServer({
2861
+ host,
2862
+ port,
2863
+ password,
2864
+ intervalMs: interval * 1e3,
2865
+ detailed,
2866
+ tlsCert,
2867
+ tlsKey,
2868
+ tlsCA,
2869
+ rateLimitMaxAttempts: rateLimit,
2870
+ rateLimitWindowMs: 6e4,
2871
+ allowedIPs: allowList,
2872
+ deniedIPs: denyList,
2873
+ auditLog: opts.p2pAuditLog,
2874
+ hmacKey: opts.p2pHmacKey
2875
+ });
2876
+ const tlsEnabled = !!(tlsCert && tlsKey);
2877
+ console.log(chalk9.bold(`
2614
2878
  pyre P2P Server`));
2615
- console.log(chalk8.dim(` Host: ${host}`));
2616
- console.log(chalk8.dim(` Port: ${port}`));
2617
- console.log(chalk8.dim(` Password: ${password}`));
2618
- console.log(chalk8.dim(` Interval: ${interval}s`));
2619
- console.log(chalk8.dim(` Detailed: ${detailed ? "on" : "off"}`));
2620
- console.log(chalk8.dim(` Waiting for peers...
2879
+ console.log(chalk9.dim(` Host: ${host}`));
2880
+ console.log(chalk9.dim(` Port: ${port}`));
2881
+ console.log(chalk9.dim(` Protocol: ${tlsEnabled ? "TLS" : "TCP"}`));
2882
+ console.log(chalk9.dim(` Password: ${password}`));
2883
+ console.log(chalk9.dim(` Interval: ${interval}s`));
2884
+ console.log(chalk9.dim(` Detailed: ${detailed ? "on" : "off"}`));
2885
+ console.log(chalk9.dim(` Rate limit: ${rateLimit} attempts/min`));
2886
+ if (allowList.length > 0) console.log(chalk9.dim(` Allowed IPs: ${allowList.join(", ")}`));
2887
+ if (denyList.length > 0) console.log(chalk9.dim(` Denied IPs: ${denyList.join(", ")}`));
2888
+ console.log(chalk9.dim(` Waiting for peers...
2621
2889
  `));
2622
2890
  await server.start();
2623
2891
  const rl = readline2.createInterface({ input: process.stdin });
@@ -2629,7 +2897,7 @@ async function runP2PServer() {
2629
2897
  process.exit(0);
2630
2898
  } else if (trimmed === "status") {
2631
2899
  const info = server.info;
2632
- console.log(chalk8.dim(` Peers connected: ${info.peersConnected}`));
2900
+ console.log(chalk9.dim(` Peers connected: ${info.peersConnected}`));
2633
2901
  }
2634
2902
  });
2635
2903
  process.once("SIGINT", () => {
@@ -2642,18 +2910,30 @@ async function runP2PConnect() {
2642
2910
  const host = opts.p2pHost || "127.0.0.1";
2643
2911
  const port = parseInt(opts.p2pPort, 10) || 9876;
2644
2912
  const password = opts.p2pPassword || "";
2913
+ const useTLS = !!opts.p2pTls;
2914
+ const tlsCA = opts.p2pCA;
2915
+ const tlsInsecure = !!opts.p2pInsecure;
2645
2916
  if (!password) {
2646
- console.log(chalk8.red("Error: a password is required for P2P connect mode. Use --p2p-password <password>"));
2917
+ console.log(chalk9.red("Error: a password is required for P2P connect mode. Use --p2p-password <password>"));
2647
2918
  process.exit(1);
2648
2919
  }
2649
- const client = new P2PClient({ host, port, password });
2920
+ const client = new P2PClient({
2921
+ host,
2922
+ port,
2923
+ password,
2924
+ tls: useTLS,
2925
+ tlsCA,
2926
+ tlsInsecure,
2927
+ auditLog: opts.p2pAuditLog,
2928
+ hmacKey: opts.p2pHmacKey
2929
+ });
2650
2930
  client.setOnStatus((msg) => {
2651
- process.stdout.write(`\r${chalk8.dim(msg)}${" ".repeat(40)}\r`);
2931
+ process.stdout.write(`\r${chalk9.dim(msg)}${" ".repeat(40)}\r`);
2652
2932
  });
2653
2933
  client.setOnData((data) => {
2654
2934
  process.stdout.write("\x1B[2J\x1B[H");
2655
- console.log(chalk8.bold(` pyre P2P Client \u2014 ${data.header.hostname}`));
2656
- console.log(chalk8.dim(` ${data.timestamp}`));
2935
+ console.log(chalk9.bold(` pyre P2P Client \u2014 ${data.header.hostname}`));
2936
+ console.log(chalk9.dim(` ${data.timestamp}`));
2657
2937
  console.log();
2658
2938
  console.log(formatTable(data, {
2659
2939
  width: process.stdout.columns || 80,
@@ -2662,15 +2942,17 @@ async function runP2PConnect() {
2662
2942
  visible: { packets: true }
2663
2943
  }));
2664
2944
  });
2665
- console.log(chalk8.bold(`
2945
+ const protocolLabel = useTLS ? "TLS" : "TCP";
2946
+ console.log(chalk9.bold(`
2666
2947
  pyre P2P Client`));
2667
- console.log(chalk8.dim(` Connecting to ${host}:${port}...`));
2668
- console.log(chalk8.dim(` Press Ctrl+C to disconnect
2948
+ console.log(chalk9.dim(` Protocol: ${protocolLabel}`));
2949
+ console.log(chalk9.dim(` Connecting to ${host}:${port}...`));
2950
+ console.log(chalk9.dim(` Press Ctrl+C to disconnect
2669
2951
  `));
2670
2952
  try {
2671
2953
  await client.connect();
2672
2954
  } catch (err) {
2673
- console.log(chalk8.red(`Connection failed: ${err.message}`));
2955
+ console.log(chalk9.red(`Connection failed: ${err.message}`));
2674
2956
  process.exit(1);
2675
2957
  }
2676
2958
  process.once("SIGINT", () => {