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.
- package/dist/index.js +414 -187
- package/package.json +1 -1
- package/src/formatters/formatters.md +164 -0
- package/src/formatters/output.ts +15 -9
- package/src/formatters/render.ts +68 -36
- package/src/history.ts +6 -0
- package/src/index.ts +20 -1
- package/src/live/input.ts +79 -57
- package/src/live/live.md +282 -0
- package/src/live/render.ts +182 -68
- package/src/live/state.ts +22 -1
- package/src/monitors/collectors.ts +126 -97
- package/src/monitors/monitors.md +165 -0
- package/src/monitors/run.ts +7 -3
- package/src/monitors/smc.ts +1 -1
- package/src/p2p/client.ts +2 -0
- package/src/p2p/p2p.md +284 -0
- package/src/p2p/server.test.ts +543 -0
- package/src/rest.md +129 -0
- package/src/sparkline.ts +8 -2
- package/src/ux.ts +62 -0
|
@@ -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.
|
package/src/monitors/run.ts
CHANGED
|
@@ -6,10 +6,14 @@
|
|
|
6
6
|
* crashes the entire collection pipeline.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/monitors/smc.ts
CHANGED
|
@@ -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 <
|
|
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.
|