pyre-cli 3.4.0 → 3.6.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.
@@ -0,0 +1,165 @@
1
+ # src/monitors/ — System Metric Collection Module
2
+
3
+ The monitors module gathers system-metric data from macOS via shell commands and SMC/powermetrics sensors. It provides `collectAll()` for a complete snapshot and individual collectors for specific metric categories.
4
+
5
+ ## Files
6
+
7
+ | File | Purpose |
8
+ |------|---------|
9
+ | `index.ts` | Barrel export — re-exports `collectAll`, `collectPower`, `collectPackets`, `collectTasks` and all types |
10
+ | `types.ts` | All TypeScript interfaces (`StatsData`, `CpuData`, `MemoryData`, etc.) |
11
+ | `smc.ts` | SMC and powermetrics sensor reader with caching |
12
+ | `collectors.ts` | Individual metric collectors and the `collectAll` orchestrator |
13
+ | `run.ts` | Shared shell-execution helper wrapping `child_process.execSync` |
14
+
15
+ ## External Dependencies
16
+
17
+ - **`node:child_process`** — `execSync` used by `run.ts` for shell command execution.
18
+ - **`chalk`** — used in `collectors.ts` for the `header.title` branding string.
19
+
20
+ ## `index.ts` — Barrel Exports
21
+
22
+ Re-exports the public API:
23
+
24
+ - Functions: `collectAll`, `collectPower`, `collectPackets`, `collectTasks`
25
+ - Types: `StatsData`, `CpuData`, `MemoryData`, `ThermalData`, `BatteryData`, `PowerData`, `DiskData`, `NetworkData`, `ProcessData`, `GpuData`, `PacketData`, `NetworkProcess`, `TaskData`
26
+
27
+ ## `types.ts` — Core Data Types
28
+
29
+ Every interface represents a distinct category of system information. `StatsData` is the aggregate that ties them together.
30
+
31
+ ### `StatsData`
32
+ The top-level aggregate consumed by all formatters and exporters:
33
+ - `header` — `{ title, hostname, os, uptime }`
34
+ - `cpu` — `CpuData`
35
+ - `gpu` — `GpuData | null`
36
+ - `memory` — `MemoryData`
37
+ - `disk` — `DiskData[]`
38
+ - `battery` — `BatteryData | null`
39
+ - `thermal` — `ThermalData`
40
+ - `network` — `NetworkData`
41
+ - `processes` — `ProcessData[]`
42
+ - `power` — `PowerData | null`
43
+ - `timestamp` — `string` (ISO 8601)
44
+ - `packets` — `PacketData | null`
45
+ - `tasks` — `TaskData[]`
46
+
47
+ ### `CpuData`
48
+ - `brand`, `cores`, `physicalCores`, `frequency` (MHz), `usage` (0–100), `loadAvg` (3-element array), `temperature?` (°C, optional — requires powermetrics)
49
+
50
+ ### `MemoryData`
51
+ - `total`, `used`, `free`, `swapTotal`, `swapUsed`, `swapFree` (bytes), `pageSize`, `usagePercent` (0–100)
52
+
53
+ ### `ThermalData`
54
+ - `state` (e.g. `"Nominal"`, `"Fair"`, `"Serious"`, `"Critical"`), `detail?`, `pressureLevel` (0–3 normalized scale), `temperatures?` (map of sensor name → °C or null), `error?`
55
+
56
+ ### `BatteryData`
57
+ - `level` (0–100), `state` (`"charging"`, `"discharging"`, `"charged"`), `timeRemaining`, `health`, `powerSource` (`"AC"` or `"Battery"`), `cycles?`, `condition?`, `maxCapacityPercent?`, `estimatedTimeToEmpty?`, `dischargeRatePerHour?`, `powerWatts?`
58
+
59
+ ### `DiskData`
60
+ - `filesystem`, `size`, `used`, `available`, `capacity` (e.g. `"85%"`), `mountpoint`
61
+
62
+ ### `NetworkData`
63
+ - `interface`, `ip`, `rxBytes`, `txBytes`, `rxPackets`, `txPackets`, `rxPacketsPerSec?`, `txPacketsPerSec?`, `connections?`, `topProcesses?`
64
+
65
+ ### `NetworkProcess`
66
+ - `pid`, `command`, `rxBytes`, `txBytes`, `rxPackets`, `txPackets`
67
+
68
+ ### `ProcessData`
69
+ - `pid`, `ppid`, `user`, `cpu` (%), `mem` (%), `command`, `state`, `threads`, `runtime` (seconds)
70
+
71
+ ### `GpuData`
72
+ - `model`, `memory` (bytes), `utilization` (%), `temperature?`, `processes` (count)
73
+
74
+ ### `PowerData`
75
+ - `cpuWatts?`, `gpuWatts?`, `combinedWatts?`
76
+
77
+ ### `PacketData`
78
+ - `totalPackets`, `rxPackets`, `txPackets`, `rxRate`, `txRate`, `connections`, `topProcesses`, `allProcesses`, `interfaces`
79
+
80
+ ### `TaskData`
81
+ - `pid`, `user`, `cpu` (%), `mem` (%), `command`, `state`, `runtime` (seconds)
82
+
83
+ ## `run.ts` — Shell Execution Helper
84
+
85
+ A thin wrapper around `child_process.execSync` with a 5-second timeout and a fallback return value.
86
+
87
+ ### `run(cmd: string, fallback?: string): Promise<string>`
88
+ Executes a shell command synchronously. Returns the trimmed stdout on success, or `fallback` (default `''`) on any error (timeout, non-zero exit, etc.). Never throws.
89
+
90
+ ## `smc.ts` — SMC and powermetrics Sensor Reader
91
+
92
+ Queries `powermetrics` for SMC temperatures, power draw, and CPU frequency. Results are cached for 4 seconds so concurrent collectors share a single invocation.
93
+
94
+ ### `SmcMetrics` interface
95
+ - `temps: Record<string, number>` — sensor name → temperature in °C (keys: `cpu_die`, `gpu_die`, `smc_die`)
96
+ - `power: { cpu?: number; gpu?: number; combined?: number }` — watts
97
+ - `freq: Record<string, number>` — frequency in Hz (keys: `e_cluster`, `p_cluster`, `cpu`)
98
+
99
+ ### `getSmcMetrics(): Promise<SmcMetrics>`
100
+ Runs `sudo -n powermetrics --samplers smc,cpu_power -n 1 --format text` and parses the output. Falls back to an empty object on any failure (missing sudoers entry, unavailable binary, etc.).
101
+
102
+ ### `parseSuffix(s: string): number`
103
+ Parses a size suffix (`K`, `M`, `G`) from `vm.swapusage` output into its byte multiplier. Case-insensitive; defaults to 1 (no suffix).
104
+
105
+ ## `collectors.ts` — Metric Collectors
106
+
107
+ Each `collect*` function queries a single category of macOS system data via shell commands and returns a strongly-typed result. All commands are wrapped in `run()` so that a failure in one category never prevents the others from returning their best-effort values.
108
+
109
+ ### Caching
110
+ Several collectors use in-memory caches with TTLs to avoid redundant shell invocations:
111
+ - `spGpuCache` — 10 s TTL for `system_profiler SPDisplaysDataType`
112
+ - `spBatteryCache` — 10 s TTL for `system_profiler SPPowerDataType`
113
+ - `netstatIbCache` — 1 s TTL for `netstat -ib`
114
+ - `routeCache` — 5 s TTL for `route -n get default` + `ifconfig`
115
+ - `sysctlCache` — 60 s TTL for `sysctl -n` queries
116
+
117
+ ### `collectAll(opts?): Promise<StatsData>`
118
+ The main entry point. Gathers every metric category in parallel via `Promise.all` and assembles the final `StatsData` object.
119
+ - `opts.detailed` — enables GPU utilization, temperature, and process data (requires powermetrics privileges)
120
+ - `opts.processLimit` — caps the number of processes returned
121
+
122
+ ### `collectPower(): Promise<PowerData | null>`
123
+ Reads power-draw metrics. Two strategies:
124
+ 1. **powermetrics** — reads CPU/GPU/combined watts from SMC (requires sudo).
125
+ 2. **ioreg fallback** — reads `AppleSmartBattery` `InstantAmperage` and `Voltage` via `ioreg`, derives watts. Handles both Intel (signed) and Apple Silicon (unsigned 64-bit) amperage representations. Sanity-clamps to < 500 W.
126
+
127
+ Returns `null` when no power data is available.
128
+
129
+ ### `collectGpu(detailed?): Promise<GpuData | null>`
130
+ Gathers GPU info from `system_profiler SPDisplaysDataType` (model, VRAM). When `detailed` is true, also queries powermetrics for GPU utilization and temperature, and counts GPU processes via `ps aux | grep`.
131
+
132
+ ### `collectCpu(): Promise<CpuData>`
133
+ Reads CPU brand from `sysctl machdep.cpu.brand_string`, core counts from `sysctl hw.ncpu`/`hw.physicalcpu`, frequency from `sysctl hw.cpufrequency` (with SMC fallback), usage from `top -l 1 -n 0`, load average from `sysctl vm.loadavg`, and temperature from SMC (if available).
134
+
135
+ ### `collectMemory(): Promise<MemoryData>`
136
+ Reads total memory from `sysctl hw.memsize`, page size from `sysctl hw.pagesize`, and page counts from `vm_stat`. Computes used/free/swap from page counts multiplied by page size. Parses `sysctl -n vm.swapusage` for swap statistics.
137
+
138
+ ### `collectDisk(): Promise<DiskData[]>`
139
+ Runs `df -h` and parses the output, filtering for entries with filesystem paths or gigabyte/terabyte sizes.
140
+
141
+ ### `collectBattery(): Promise<BatteryData | null>`
142
+ Reads battery state from `pmset -g batt` (level, state, time remaining, power source). Enriches with `system_profiler SPPowerDataType` for cycle count, condition, and max capacity percent. Calls `estimateBatteryLife()` for discharge rate and estimated time to empty.
143
+
144
+ ### `estimateBatteryLife(level, state)`
145
+ Uses a 15-minute rolling window of battery level samples to compute discharge rate (%/hour) and estimate time to empty. Only trusts estimates after at least 30 seconds of data. Resets the window on state changes (charging → discharging).
146
+
147
+ ### `collectThermal(detailed?): Promise<ThermalData>`
148
+ Reads thermal state from `pmset -g therm`. Parses the `Thermal state:` line or `CPU_Speed_Limit` value to determine state (`Nominal`, `Fair`, `Serious`, `Critical`). When `detailed` is true, also reads per-sensor temperatures from SMC. Falls back to temperature-based state estimation if pmset provides no usable data.
149
+
150
+ ### `collectNetwork(): Promise<NetworkData>`
151
+ Determines the primary network interface via `route -n get default` + `ifconfig`, then reads RX/TX bytes and packets from `netstat -ib` for that interface. Results are cached for 1 second.
152
+
153
+ ### `collectProcesses(limit?): Promise<ProcessData[]>`
154
+ Runs `ps -eo pid,ppid,user,pcpu,pmem,state,threads,time,comm` sorted by CPU usage (descending). Parses each line into a `ProcessData` object. Limits results when `limit` is provided.
155
+
156
+ ### `collectPackets(): Promise<PacketData | null>`
157
+ Aggregates network packet statistics from `netstat -ib` (per-interface and total RX/TX packets/bytes). Counts TCP connections via `netstat -an | grep ESTABLISHED`. Identifies top network processes via `lsof -i -n -P`.
158
+
159
+ ### `collectTasks(limit = 12): Promise<TaskData[]>`
160
+ Runs `ps -eo pid,user,pcpu,pmem,state,time,comm` sorted by CPU usage, limited to `limit` rows. Parses each line into a `TaskData` object.
161
+
162
+ ## `run.ts` — Shell Execution Helper
163
+
164
+ ### `run(cmd: string, fallback?: string): Promise<string>`
165
+ Executes a shell command synchronously with a 5-second timeout. Returns trimmed stdout on success, or `fallback` (default `''`) on any error. Never throws.
@@ -6,10 +6,14 @@
6
6
  * crashes the entire collection pipeline.
7
7
  */
8
8
 
9
- async function run(cmd: string, fallback: string = ''): Promise<string> {
9
+ import { exec } from 'node:child_process';
10
+ import { promisify } from 'node:util';
11
+
12
+ const execAsync = promisify(exec);
13
+
14
+ async function run(cmd: string, fallback: string = '', timeout = 5000): Promise<string> {
10
15
  try {
11
- const { execSync } = await import('node:child_process');
12
- return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000 }).trim();
16
+ return (await execAsync(cmd, { encoding: 'utf8', timeout })).stdout.trim();
13
17
  } catch {
14
18
  return fallback;
15
19
  }
@@ -33,7 +33,7 @@ let smcCache: { data: SmcMetrics; ts: number } | null = null;
33
33
  */
34
34
  export async function getSmcMetrics(): Promise<SmcMetrics> {
35
35
  const now = Date.now();
36
- if (smcCache && now - smcCache.ts < 1500) return smcCache.data;
36
+ if (smcCache && now - smcCache.ts < 4000) return smcCache.data;
37
37
 
38
38
  const result: SmcMetrics = { temps: {}, power: {}, freq: {} };
39
39
  try {
package/src/p2p/client.ts CHANGED
@@ -234,6 +234,8 @@ export class P2PClient {
234
234
  this.status('Connection refused — is the server running?');
235
235
  } else if (code === 'ETIMEDOUT') {
236
236
  this.status('Connection timed out');
237
+ } else if (code === 'EHOSTDOWN' || code === 'EHOSTUNREACH') {
238
+ this.status(`Host unreachable — ${err.message}`);
237
239
  } else if (code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
238
240
  this.status('TLS certificate verification failed — use --p2p-insecure to skip');
239
241
  } else {
package/src/p2p/p2p.md ADDED
@@ -0,0 +1,284 @@
1
+ # src/p2p/ — P2P Live Data Transfer Module
2
+
3
+ The P2P module enables live system-metric data streaming between two machines over a TCP (or TLS) connection. It implements a challenge-response authentication protocol with HMAC message signing, and supports optional audit logging.
4
+
5
+ ## Files
6
+
7
+ | File | Purpose |
8
+ |------|---------|
9
+ | `index.ts` | Barrel export — re-exports `P2PServer`, `startP2PServer`, `P2PClient`, `connectP2PClient`, and types |
10
+ | `types.ts` | Type definitions for messages, options, and connection info |
11
+ | `protocol.ts` | Message encoding/decoding, HMAC signing/verification, nonce generation |
12
+ | `server.ts` | `P2PServer` class — listens for connections, authenticates peers, streams live data |
13
+ | `client.ts` — `P2PClient` class — connects to a server, authenticates, receives live data |
14
+
15
+ ## External Dependencies
16
+
17
+ - **`src/monitors/index.ts`** — `collectAll()` used by the server to gather data for streaming.
18
+ - **`src/monitors/types.ts`** — `StatsData` type for streamed data payloads.
19
+ - **`node:net`** — TCP server/client sockets.
20
+ - **`node:tls`** — TLS server/client for encrypted connections.
21
+ - **`node:crypto`** — HMAC signing, SHA-256 hashing, nonce generation.
22
+ - **`node:fs`** — Audit log file writing.
23
+
24
+ ## `index.ts` — Barrel Exports
25
+
26
+ Re-exports the public API:
27
+
28
+ - **Server:** `P2PServer`, `startP2PServer(options)`
29
+ - **Client:** `P2PClient`, `connectP2PClient(options)`
30
+ - **Types:** `P2PServerOptions`, `P2PClientOptions`, `P2PConnectionInfo`
31
+
32
+ ## `types.ts` — Type Definitions
33
+
34
+ ### `P2PMessageKind`
35
+ Union type for all message types exchanged between server and client:
36
+ `'auth' | 'auth-ok' | 'auth-fail' | 'data' | 'ping' | 'pong' | 'disconnect' | 'challenge'`
37
+
38
+ ### `P2PMessage`
39
+ Envelope for all messages on the wire:
40
+ - `kind: P2PMessageKind` — message type
41
+ - `payload: unknown` — type-specific payload
42
+ - `hmac?: string` — HMAC signature (present on signed messages)
43
+
44
+ ### Message Payload Types
45
+
46
+ | Type | Shape | Used By |
47
+ |------|-------|---------|
48
+ | `ChallengeMessage` | `{ nonce: string }` | Server → Client (challenge) |
49
+ | `AuthMessage` | `{ passwordHash: string; nonce: string }` | Client → Server (auth response) |
50
+ | `AuthOkMessage` | `{ ok: true }` | Server → Client (auth success) |
51
+ | `AuthFailMessage` | `{ ok: false; reason: string }` | Server → Client (auth failure) |
52
+ | `DataMessage` | `{ stats: StatsData }` | Server → Client (live data) |
53
+ | `PingMessage` | `{ ts: number }` | Server → Client / Client → Server |
54
+ | `PongMessage` | `{ ts: number }` | Client → Server / Server → Client |
55
+ | `DisconnectMessage` | `{ reason: string }` | Either side |
56
+
57
+ ### `P2PPayload`
58
+ Union of all payload types.
59
+
60
+ ### `P2PServerOptions`
61
+ Configuration for the P2P server:
62
+ - `host: string` — bind address
63
+ - `port: number` — listen port
64
+ - `password: string` — shared secret for authentication
65
+ - `intervalMs: number` — data-stream interval in milliseconds
66
+ - `detailed: boolean` — pass through to `collectAll` for detailed sensor data
67
+ - `tlsCert?`, `tlsKey?`, `tlsCA?` — TLS certificate paths for encrypted connections
68
+ - `rateLimitMaxAttempts?` — max auth attempts per IP per window (default 5)
69
+ - `rateLimitWindowMs?` — time window for rate limiting in ms (default 60000)
70
+ - `allowedIPs?` — whitelist of allowed IPs (empty = all)
71
+ - `deniedIPs?` — blacklist of denied IPs
72
+ - `auditLog?` — directory for audit log files
73
+ - `hmacKey?` — HMAC key override (derived from password if not provided)
74
+ - `onLog?` — callback for log messages
75
+ - `onPeerEvent?` — callback for peer connection/auth/disconnect/error events
76
+
77
+ ### `P2PClientOptions`
78
+ Configuration for the P2P client:
79
+ - `host: string` — server address
80
+ - `port: number` — server port
81
+ - `password: string` — shared secret for authentication
82
+ - `tls?` — enable TLS connection
83
+ - `tlsCA?` — CA certificate path for TLS verification
84
+ - `tlsInsecure?` — skip TLS certificate verification
85
+ - `auditLog?` — directory for audit log files
86
+ - `hmacKey?` — HMAC key override (derived from password if not provided)
87
+
88
+ ### `P2PConnectionInfo`
89
+ Runtime connection info:
90
+ - `host: string`
91
+ - `port: number`
92
+ - `peersConnected: number`
93
+
94
+ ## `protocol.ts` — Wire Protocol
95
+
96
+ Handles message serialization, HMAC signing/verification, and the binary wire format.
97
+
98
+ ### Wire Format
99
+ Each message on the wire is:
100
+ 1. **4-byte header** — big-endian `UInt32` indicating the body length in bytes
101
+ 2. **Body** — UTF-8 JSON of the `P2PMessage` envelope (including `hmac` if present)
102
+
103
+ ### `generateNonce(): string`
104
+ Generates a 32-byte random hex nonce for challenge-response authentication.
105
+
106
+ ### `hashPassword(password: string, nonce: string): string`
107
+ Computes `SHA-256(password + ":" + nonce)` as a hex string. Used for challenge-response authentication (never transmits the raw password).
108
+
109
+ ### `computeHMAC(body: string, key?: string): string | undefined`
110
+ Computes an HMAC-SHA256 of the message body using the provided key. Returns `undefined` if no key is provided.
111
+
112
+ ### `verifyHMAC(body: string, expectedHmac: string, key?: string): boolean`
113
+ Verifies an HMAC signature using timing-safe comparison. Returns `true` if the computed HMAC matches the expected value. Returns `true` if no key is provided and no HMAC is expected.
114
+
115
+ ### `signMessage(kind, payload, key?): Buffer`
116
+ Builds a `P2PMessage` envelope, computes its HMAC, and serializes it to the binary wire format (4-byte header + JSON body).
117
+
118
+ ### `encodeChallenge(nonce): Buffer`
119
+ Encodes a `challenge` message.
120
+
121
+ ### `encodeAuth(passwordHash, nonce): Buffer`
122
+ Encodes an `auth` message.
123
+
124
+ ### `encodeAuthOk(): Buffer`
125
+ Encodes an `auth-ok` message.
126
+
127
+ ### `encodeAuthFail(reason): Buffer`
128
+ Encodes an `auth-fail` message.
129
+
130
+ ### `encodeData(stats): Buffer`
131
+ Encodes a `data` message containing `StatsData`.
132
+
133
+ ### `encodePing(): Buffer`
134
+ Encodes a `ping` message with the current timestamp.
135
+
136
+ ### `encodePong(ts): Buffer`
137
+ Encodes a `pong` message with the given timestamp.
138
+
139
+ ### `encodeDisconnect(reason): Buffer`
140
+ Encodes a `disconnect` message.
141
+
142
+ ### `decodeMessage(buf: Buffer): P2PMessage`
143
+ Deserializes a binary message body (UTF-8 JSON) into a `P2PMessage` object.
144
+
145
+ ### `readMessage(stream): Buffer | null`
146
+ Reads a single message from a stream: reads the 4-byte header, then reads the body of the indicated length. Throws if the message exceeds `MAX_MESSAGE_SIZE` (16 MB).
147
+
148
+ ### `readAll(stream, total): Buffer`
149
+ Reads exactly `total` bytes from a stream, accumulating chunks into a single `Buffer`.
150
+
151
+ ## `server.ts` — P2P Server
152
+
153
+ ### `P2PServer` Class
154
+
155
+ #### Constructor
156
+ `new P2PServer(options: P2PServerOptions)` — stores options and derives the HMAC key from the password via SHA-256.
157
+
158
+ #### Properties
159
+ - `peerEventHistory` — the last 200 peer events (type, detail, timestamp)
160
+ - `info` — `P2PConnectionInfo` with host, port, and connected peer count
161
+
162
+ #### `start(): Promise<void>`
163
+ Starts the server:
164
+ 1. Opens an audit log file if `options.auditLog` is set.
165
+ 2. Creates a `net.Server` (or `tls.Server` if TLS cert/key are provided).
166
+ 3. Listens on `options.host:options.port`.
167
+ 4. On each new connection, calls `onConnection()`.
168
+
169
+ #### `stop(): void`
170
+ Stops the server:
171
+ 1. Disconnects all authenticated peers with a `disconnect` message.
172
+ 2. Clears all peers, rate-limit entries, and peer events.
173
+ 3. Closes the server socket and audit stream.
174
+
175
+ #### `onConnection(socket)`
176
+ Handles a new TCP connection:
177
+ 1. Checks the peer IP against `allowedIPs`/`deniedIPs` — blocks if denied.
178
+ 2. Checks rate limits — drops connections exceeding `rateLimitMaxAttempts` within `rateLimitWindowMs`.
179
+ 3. Creates a `Peer` state object with a read buffer, header/message parsing state, and `pendingNonce`.
180
+ 4. Sends a `challenge` message with a fresh nonce.
181
+ 5. Sets a 30-second socket timeout (disconnects unauthenticated peers).
182
+ 6. Registers `data`, `close`, and `error` handlers.
183
+
184
+ #### `onData(peer, data)`
185
+ Appends incoming data to the peer's read buffer. For unauthenticated peers, attempts to read and process a complete message (expects `auth` first).
186
+
187
+ #### `handleAuthMessage(peer, body)`
188
+ Processes an authentication message:
189
+ 1. Decodes the message and verifies it's an `auth` kind.
190
+ 2. Validates the payload contains `passwordHash` and `nonce`.
191
+ 3. Checks nonce matches the pending challenge nonce.
192
+ 4. Computes the expected password hash via `hashPassword(options.password, nonce)` and compares.
193
+ 5. On success: marks peer as authenticated, sends `auth-ok`, starts the data stream and ping interval.
194
+ 6. On failure: sends `auth-fail` with a reason and disconnects.
195
+
196
+ #### `startDataStream(peer)`
197
+ Begins streaming `StatsData` to an authenticated peer:
198
+ 1. Sends an immediate data snapshot.
199
+ 2. Sets up an interval to send data every `options.intervalMs` milliseconds.
200
+ 3. Each data message is signed with `signMessage('data', { stats }, hmacKey)`.
201
+
202
+ #### `startPing(peer)`
203
+ Sends a `ping` message every 5 seconds to keep the connection alive.
204
+
205
+ #### `onPeerClose(peer)` / `onPeerError(peer, err)`
206
+ Cleans up peer resources (interval handles, ping handles) and emits disconnect/error events.
207
+
208
+ #### `disconnectPeer(peer, reason)`
209
+ Sends a `disconnect` message and destroys the socket.
210
+
211
+ #### `checkRateLimit(ip)` / `cleanupRateLimit(ip)`
212
+ Tracks auth attempts per IP in a sliding window. Blocks IPs that exceed the threshold.
213
+
214
+ #### `audit(ip, event, detail)`
215
+ Writes an audit log entry if an audit stream is open.
216
+
217
+ ### `startP2PServer(options): Promise<P2PServer>`
218
+ Convenience function that creates a `P2PServer`, starts it, and returns the instance.
219
+
220
+ ## `client.ts` — P2P Client
221
+
222
+ ### `P2PClient` Class
223
+
224
+ #### Constructor
225
+ `new P2PClient(options: P2PClientOptions)` — stores options and derives the HMAC key from the password.
226
+
227
+ #### Callbacks
228
+ - `setOnData(cb: (data: StatsData) => void)` — called when a `data` message is received from the server.
229
+ - `setOnStatus(cb: (msg: string) => void)` — called for status updates (connecting, authenticating, errors, etc.).
230
+
231
+ #### `connect(): Promise<void>`
232
+ Connects to the P2P server:
233
+ 1. Opens an audit log file if `options.auditLog` is set.
234
+ 2. Creates a `net.Socket` (or `tls.TLSSocket` if TLS is enabled).
235
+ 3. Connects to `options.host:options.port`.
236
+ 4. Registers `data`, `close`, `error`, and `timeout` handlers.
237
+ 5. The connection promise resolves when the socket connects (not when authenticated).
238
+
239
+ #### `disconnect(): void`
240
+ Gracefully disconnects:
241
+ 1. Sends a `disconnect` message.
242
+ 2. Destroys the socket.
243
+ 3. Clears reconnect and ping timers.
244
+ 4. Closes the audit stream.
245
+
246
+ #### `onData(data: Buffer)`
247
+ Handles incoming data from the server:
248
+ 1. Appends to the read buffer.
249
+ 2. Loops: reads 4-byte header, then reads the body of the indicated length.
250
+ 3. For each complete message, calls `handleMessage()`.
251
+
252
+ #### `handleMessage(body: Buffer)`
253
+ Processes a complete message:
254
+ 1. Verifies the HMAC signature — rejects if verification fails.
255
+ 2. Dispatches based on `msg.kind`:
256
+ - `challenge` — computes password hash, sends `auth` message.
257
+ - `auth-ok` — starts ping interval.
258
+ - `auth-fail` — shows error and disconnects.
259
+ - `data` — calls `onDataCallback` with the `StatsData` payload.
260
+ - `ping` — responds with `pong`.
261
+ - `pong` — no action (keep-alive acknowledgment).
262
+ - `disconnect` — shows server reason and disconnects.
263
+
264
+ #### `startPing()`
265
+ Sends a `ping` message every 15 seconds.
266
+
267
+ #### `onClose()`
268
+ If the client was running, schedules a reconnect after 3 seconds.
269
+
270
+ #### `onError(err)`
271
+ Maps socket error codes to user-friendly status messages:
272
+ - `ECONNREFUSED` — "Connection refused — is the server running?"
273
+ - `ETIMEDOUT` — "Connection timed out"
274
+ - `UNABLE_TO_VERIFY_LEAF_SIGNATURE` — TLS cert error, suggests `--p2p-insecure`
275
+ - Other errors — shows the error message
276
+
277
+ #### `onTimeout()`
278
+ Treats a socket timeout as a disconnect and triggers reconnection.
279
+
280
+ #### `audit(event, detail)`
281
+ Writes an audit log entry if an audit stream is open.
282
+
283
+ ### `connectP2PClient(options): Promise<P2PClient>`
284
+ Convenience function that creates a `P2PClient`, connects it, and returns the instance.