@singhak/nodeui-core 0.1.0 → 0.3.1

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.d.mts CHANGED
@@ -1,5 +1,5 @@
1
+ import { IncomingMessage, ServerResponse } from 'node:http';
1
2
  import { Readable } from 'node:stream';
2
- import { ServerResponse, IncomingMessage } from 'node:http';
3
3
 
4
4
  /**
5
5
  * Shared typed contract for the NodeUI developer console.
@@ -8,7 +8,12 @@ import { ServerResponse, IncomingMessage } from 'node:http';
8
8
  * contract. Every provider returns a {@link ProviderResult}; every REST
9
9
  * endpoint returns an {@link ApiEnvelope} which is the same shape.
10
10
  */
11
- type PanelId = 'health' | 'memory' | 'cpu' | 'event-loop' | 'heap-snapshot' | 'startup' | 'requests' | 'env' | 'routes' | 'logs' | 'metrics';
11
+ type BuiltInPanelId = 'health' | 'memory' | 'cpu' | 'event-loop' | 'heap-snapshot' | 'startup' | 'requests' | 'env' | 'routes' | 'logs' | 'metrics' | 'outgoing';
12
+ /**
13
+ * Panel identifier. Built-in panels are listed in {@link BuiltInPanelId};
14
+ * plugins may register any other string id (lowercase letters, digits, `-`).
15
+ */
16
+ type PanelId = BuiltInPanelId | (string & {});
12
17
  /** Resolved, validated console configuration. */
13
18
  interface NodeUIConfig {
14
19
  /** URL path prefix where the console and its API are served. Default `/nodeui`. */
@@ -35,6 +40,18 @@ interface NodeUIConfig {
35
40
  confirmTtlMs: number;
36
41
  /** Directory where heap snapshot files are written. */
37
42
  heapSnapshotDir: string;
43
+ /** Extra `Host` header hostnames accepted besides loopback names. */
44
+ allowedHosts: string[];
45
+ /** Extra `Origin` values accepted for cross-origin calls. */
46
+ allowedOrigins: string[];
47
+ /** Extra remote IPs / IPv4 CIDRs accepted (e.g. Docker bridge `172.17.0.0/16`). */
48
+ allowedRemoteAddresses: string[];
49
+ /** Accept requests carrying `X-Forwarded-*` headers. */
50
+ trustProxy: boolean;
51
+ /** Whether a shared access token is required. */
52
+ authRequired: boolean;
53
+ /** Maximum concurrent live (SSE) streams. */
54
+ maxSseClients: number;
38
55
  }
39
56
  /** Context handed to every provider call. */
40
57
  interface ProviderContext {
@@ -72,6 +89,8 @@ type ApiEnvelope<T> = ProviderResult<T>;
72
89
  */
73
90
  interface NodeUIProvider<T = unknown> {
74
91
  readonly id: PanelId;
92
+ /** Display label for plugin panels; defaults to the id. */
93
+ readonly title?: string;
75
94
  start?(ctx: ProviderContext): void;
76
95
  stop?(ctx: ProviderContext): void;
77
96
  get(ctx: ProviderContext): ProviderResult<T> | Promise<ProviderResult<T>>;
@@ -99,6 +118,12 @@ interface CpuData {
99
118
  totalPercent: number;
100
119
  sampleAtMs: number;
101
120
  }
121
+ interface HealthCheckResult {
122
+ name: string;
123
+ status: 'up' | 'down';
124
+ durationMs: number;
125
+ error?: string;
126
+ }
102
127
  interface HealthData {
103
128
  status: 'ok' | 'degraded' | 'critical' | 'unknown';
104
129
  statusReason: string;
@@ -108,6 +133,8 @@ interface HealthData {
108
133
  platform: string;
109
134
  eventLoopLagMs: number | null;
110
135
  memoryUsedPercent: number | null;
136
+ /** Results of user-registered dependency checks; empty when none. */
137
+ checks: HealthCheckResult[];
111
138
  }
112
139
  interface StartupMark {
113
140
  name: string;
@@ -128,9 +155,37 @@ interface RequestEntry {
128
155
  timestampMs: number;
129
156
  ip: string;
130
157
  }
158
+ interface RouteStat {
159
+ method: string;
160
+ path: string;
161
+ count: number;
162
+ avgMs: number;
163
+ p95Ms: number;
164
+ errors: number;
165
+ }
166
+ interface RequestsSummary {
167
+ /** Requests currently held in the ring buffer. */
168
+ count: number;
169
+ errors: number;
170
+ /** `errors / count` in 0..1 (5xx only). */
171
+ errorRate: number;
172
+ avgMs: number;
173
+ p50Ms: number;
174
+ p95Ms: number;
175
+ p99Ms: number;
176
+ byStatus: {
177
+ '2xx': number;
178
+ '3xx': number;
179
+ '4xx': number;
180
+ '5xx': number;
181
+ };
182
+ /** Busiest/slowest routes, slowest p95 first (at most 8). */
183
+ routes: RouteStat[];
184
+ }
131
185
  interface RequestsData {
132
186
  total: number;
133
187
  entries: RequestEntry[];
188
+ summary: RequestsSummary;
134
189
  }
135
190
  interface EnvEntry {
136
191
  key: string;
@@ -165,6 +220,21 @@ interface MetricsBucket {
165
220
  interface MetricsData {
166
221
  buckets: MetricsBucket[];
167
222
  }
223
+ interface OutgoingRequestEntry {
224
+ id: number;
225
+ method: string;
226
+ /** Target as `host/path` (query string removed). */
227
+ url: string;
228
+ status: number | null;
229
+ durationMs: number;
230
+ timestampMs: number;
231
+ error?: string;
232
+ }
233
+ interface OutgoingData {
234
+ total: number;
235
+ failed: number;
236
+ entries: OutgoingRequestEntry[];
237
+ }
168
238
  interface HeapSnapshotData {
169
239
  fileName: string;
170
240
  filePath: string;
@@ -194,6 +264,12 @@ interface ConfigData {
194
264
  enabled: boolean;
195
265
  pattern: string;
196
266
  };
267
+ locked: boolean;
268
+ /** Plugin panel ids and titles, in registration order. */
269
+ plugins: Array<{
270
+ id: PanelId;
271
+ title: string;
272
+ }>;
197
273
  }
198
274
 
199
275
  /** Replacement value used when masking secret values. */
@@ -222,6 +298,12 @@ declare class RingBuffer<T> {
222
298
 
223
299
  /** Keys whose values are redacted in any panel output. */
224
300
  declare const SECRET_KEY_PATTERN: RegExp;
301
+ /**
302
+ * Redacts secrets embedded in free text: credentials in URLs
303
+ * (`postgres://user:pass@host`), bearer/basic tokens, JWTs and
304
+ * `password=...`-style pairs. Used for log lines and any string value.
305
+ */
306
+ declare function maskSecretText(text: string): string;
225
307
  interface ActivationDecision {
226
308
  active: boolean;
227
309
  reason: string;
@@ -234,12 +316,53 @@ interface ActivationDecision {
234
316
  declare function resolveActivation(env: Record<string, string | undefined>): ActivationDecision;
235
317
  /** True when the socket address is a loopback address. */
236
318
  declare function isLoopbackAddress(address: string | undefined): boolean;
319
+ /**
320
+ * True when `address` matches one of `allowed`, each an exact IP or an IPv4
321
+ * CIDR (e.g. `172.17.0.0/16`, the default Docker bridge network).
322
+ */
323
+ declare function matchesAddress(address: string | undefined, allowed: readonly string[]): boolean;
324
+ /** Extracts the lower-cased hostname from a `Host` header value (no port). */
325
+ declare function hostnameFromHostHeader(value: string | undefined): string | null;
326
+ /** True for the hostnames a browser uses to reach a loopback server. */
327
+ declare function isLoopbackHostname(hostname: string | null): boolean;
237
328
  /**
238
329
  * Deep-clone a value, replacing any value under a key matching the secret
239
- * pattern with `[REDACTED]`. The input is not mutated.
330
+ * pattern with `[REDACTED]` and scrubbing secrets embedded in string values
331
+ * (URL credentials, bearer tokens, `password=...`). The input is not mutated.
240
332
  */
241
333
  declare function maskSecrets<T>(value: T, pattern?: RegExp): T;
242
334
 
335
+ declare const AUTH_COOKIE = "nodeui_token";
336
+ interface GuardOptions {
337
+ /** Configured console host; a loopback value enables the strict checks. */
338
+ host: string;
339
+ /** Extra `Host` header hostnames accepted (e.g. `myapp.local`). */
340
+ allowedHosts: readonly string[];
341
+ /** Extra `Origin` values accepted for cross-origin calls. */
342
+ allowedOrigins: readonly string[];
343
+ /** Extra remote IPs / IPv4 CIDRs accepted (e.g. `172.17.0.0/16` for Docker). */
344
+ allowedRemoteAddresses: readonly string[];
345
+ /** Accept requests carrying `X-Forwarded-*` headers. Default false. */
346
+ trustProxy: boolean;
347
+ /** When set, every request must present this token. */
348
+ authToken?: string;
349
+ }
350
+ type GuardResult = {
351
+ ok: true;
352
+ setToken?: string;
353
+ } | {
354
+ ok: false;
355
+ status: number;
356
+ code: string;
357
+ message: string;
358
+ };
359
+ /**
360
+ * Builds the request guard for the console. It combines: loopback/remote
361
+ * address policy, `Host` validation (DNS-rebinding defence), `Origin`
362
+ * validation (cross-site request defence) and an optional shared token.
363
+ */
364
+ declare function createGuard(options: GuardOptions): (req: IncomingMessage) => GuardResult;
365
+
243
366
  /**
244
367
  * Single-use, expiring confirmation nonces. Mutating actions (such as heap
245
368
  * snapshots) require a nonce issued here, presented via the
@@ -247,8 +370,9 @@ declare function maskSecrets<T>(value: T, pattern?: RegExp): T;
247
370
  */
248
371
  declare class ConfirmationStore {
249
372
  private readonly ttlMs;
373
+ private readonly maxOutstanding;
250
374
  private nonces;
251
- constructor(ttlMs: number);
375
+ constructor(ttlMs: number, maxOutstanding?: number);
252
376
  issue(): ConfirmIssued;
253
377
  /** Consumes `nonce` and returns true only if it was valid and unexpired. */
254
378
  consume(nonce: string): boolean;
@@ -381,12 +505,45 @@ declare class EventLoopLagProvider implements NodeUIProvider<EventLoopSample> {
381
505
  };
382
506
  }
383
507
 
384
- /** Derives process + event-loop health from the latest sampled values. */
508
+ /**
509
+ * A dependency check (database ping, cache ping, ...). Resolve for healthy,
510
+ * reject/throw (or resolve `false`) for unhealthy.
511
+ */
512
+ type HealthCheck = () => unknown | Promise<unknown>;
513
+ /** Derives process + event-loop health, plus optional dependency checks. */
385
514
  declare class HealthProvider implements NodeUIProvider<HealthData> {
515
+ private readonly checks;
386
516
  readonly id: "health";
387
- get(ctx: ProviderContext): {
517
+ private cache;
518
+ constructor(checks?: Record<string, HealthCheck>);
519
+ private runChecks;
520
+ get(ctx: ProviderContext): Promise<{
388
521
  ok: true;
389
522
  data: HealthData;
523
+ }>;
524
+ }
525
+
526
+ type Recorder = (entry: Omit<OutgoingRequestEntry, 'id'>) => void;
527
+ /**
528
+ * Refcounted instrumentation of outgoing HTTP calls made via `http`,
529
+ * `https` (axios, got, node-fetch, ...) and global `fetch` (undici). The
530
+ * first consumer installs the hooks, the last release restores everything.
531
+ */
532
+ declare function interceptOutgoing(record: Recorder): () => void;
533
+ /** Recent outgoing HTTP calls, recorded lazily while the panel is active. */
534
+ declare class OutgoingProvider implements NodeUIProvider<OutgoingData> {
535
+ readonly id: "outgoing";
536
+ private buffer;
537
+ private nextId;
538
+ private failed;
539
+ private release;
540
+ constructor(size: number);
541
+ start(): void;
542
+ stop(): void;
543
+ record(entry: Omit<OutgoingRequestEntry, 'id'>): void;
544
+ get(): {
545
+ ok: true;
546
+ data: OutgoingData;
390
547
  };
391
548
  }
392
549
 
@@ -405,6 +562,7 @@ declare class HeapSnapshotProvider implements NodeUIProvider<HeapSnapshotPanelDa
405
562
  takeSnapshot(ctx: ProviderContext): Promise<ProviderResult<HeapSnapshotData>>;
406
563
  }
407
564
 
565
+ declare function summarizeRequests(entries: readonly RequestEntry[]): RequestsSummary;
408
566
  /** Fixed-size ring buffer of recent HTTP requests, recorded via middleware hook. */
409
567
  declare class RequestsProvider implements NodeUIProvider<RequestsData> {
410
568
  readonly id: "requests";
@@ -521,7 +679,7 @@ interface SseStream {
521
679
  * Minimal Server-Sent Events writer over a raw HTTP response. Events are
522
680
  * emitted as `data: <json>\n\n`; heartbeats as `:ping\n\n`.
523
681
  */
524
- declare function startSse(res: ServerResponse): SseStream;
682
+ declare function startSse(res: ServerResponse, extraHeaders?: Record<string, string>): SseStream;
525
683
 
526
684
  interface NodeUIOptions {
527
685
  /** URL path prefix. Default `/nodeui` (or `NODEUI_PATH`). */
@@ -548,8 +706,32 @@ interface NodeUIOptions {
548
706
  inactivityTimeoutMs?: number;
549
707
  /** TTL for mutation confirmation nonces. Default 60000. */
550
708
  confirmTtlMs?: number;
551
- /** Directory for heap snapshot files. Defaults to the OS temp dir. */
709
+ /** Directory for heap snapshot files. Defaults to a private dir under the OS temp dir. */
552
710
  heapSnapshotDir?: string;
711
+ /** Extra `Host` hostnames accepted besides loopback names (or `NODEUI_ALLOWED_HOSTS`, comma-separated). */
712
+ allowedHosts?: string[];
713
+ /** Extra `Origin` values accepted for cross-origin calls (or `NODEUI_ALLOWED_ORIGINS`). */
714
+ allowedOrigins?: string[];
715
+ /**
716
+ * Extra remote IPs / IPv4 CIDRs accepted (or `NODEUI_ALLOWED_REMOTE`). Use
717
+ * `['172.16.0.0/12']` to reach the console from the host when the app runs in Docker.
718
+ */
719
+ allowedRemoteAddresses?: string[];
720
+ /** Accept requests carrying `X-Forwarded-*` headers (or `NODEUI_TRUST_PROXY=true`). Default false. */
721
+ trustProxy?: boolean;
722
+ /**
723
+ * Shared access token (or `NODEUI_TOKEN`). When set, open `/nodeui/?token=<token>` once;
724
+ * the browser then keeps an HttpOnly cookie. API clients may send `Authorization: Bearer`.
725
+ */
726
+ authToken?: string;
727
+ /** Maximum concurrent live (SSE) streams. Default 10. */
728
+ maxSseClients?: number;
729
+ /** Custom panels; each provider needs a unique lowercase id, e.g. `queues`. */
730
+ plugins?: NodeUIProvider[];
731
+ /** Named dependency checks (database, cache, ...) surfaced in the health panel. */
732
+ healthChecks?: Record<string, HealthCheck>;
733
+ /** Max recorded outgoing HTTP calls. Default 200. */
734
+ outgoingLogSize?: number;
553
735
  }
554
736
  interface NodeUIServer {
555
737
  readonly config: NodeUIConfig;
@@ -559,6 +741,11 @@ interface NodeUIServer {
559
741
  middleware(): NodeUIMiddleware;
560
742
  /** Direct request handling (no `next`); used for tests and adapters. */
561
743
  handle(req: IncomingMessage, res: ServerResponse): Promise<void>;
744
+ /**
745
+ * Supplies the route list directly (frameworks other than Express, e.g.
746
+ * Fastify's `onRoute`). Takes precedence over Express router introspection.
747
+ */
748
+ setRoutes(source: RouteEntry[] | (() => RouteEntry[])): void;
562
749
  /** Records a bootstrap timing mark, e.g. `mark("listening")`. */
563
750
  mark(name: string): void;
564
751
  /** Whether a background-sampling provider is currently running. */
@@ -576,7 +763,7 @@ type NodeUIMiddleware = (req: IncomingMessage, res: ServerResponse, next: () =>
576
763
  * Serializes an API envelope to JSON, applying secret masking so values
577
764
  * under keys like `TOKEN`/`KEY`/`SECRET`/`PASSWORD` never reach the UI.
578
765
  */
579
- declare function serializeEnvelope<T>(envelope: ApiEnvelope<T>): string;
766
+ declare function serializeEnvelope<T>(envelope: ApiEnvelope<T>, mask?: boolean): string;
580
767
  declare function createNodeUI(options?: NodeUIOptions): NodeUIServer;
581
768
 
582
- export { type ActivationDecision, type ApiEnvelope, type ConfigData, type ConfirmIssued, ConfirmationStore, type CpuClock, type CpuData, CpuProvider, type CpuUsageSnapshot, DEFAULT_LOG_SIZE, type EnvData, type EnvEntry, EnvProvider, type EventLoopClock, EventLoopLagProvider, type EventLoopSample, type HealthData, HealthProvider, type HeapSnapshotData, type HeapSnapshotPanelData, HeapSnapshotProvider, type LogEntry, type LogLevel, type LogsData, LogsProvider, type MemoryData, MemoryProvider, type MetricsBucket, type MetricsData, MetricsProvider, type NodeUIConfig, type NodeUIMiddleware, type NodeUIOptions, type NodeUIProvider, type NodeUIServer, type PanelId, type ProviderContext, type ProviderError, ProviderRegistry, type ProviderResult, type RequestEntry, type RequestsData, RequestsProvider, RingBuffer, type RouteEntry, type RoutesData, RoutesProvider, SECRET_KEY_PATTERN, SECRET_MASKED, Sampler, type SamplerOptions, type SseStream, type StartupData, type StartupMark, StartupTracker, type StaticAsset, createNodeUI, extractRoutes, interceptConsole, isLoopbackAddress, maskSecrets, resolveActivation, resolveStaticAsset, serializeEnvelope, startSse };
769
+ export { AUTH_COOKIE, type ActivationDecision, type ApiEnvelope, type BuiltInPanelId, type ConfigData, type ConfirmIssued, ConfirmationStore, type CpuClock, type CpuData, CpuProvider, type CpuUsageSnapshot, DEFAULT_LOG_SIZE, type EnvData, type EnvEntry, EnvProvider, type EventLoopClock, EventLoopLagProvider, type EventLoopSample, type GuardOptions, type GuardResult, type HealthCheck, type HealthCheckResult, type HealthData, HealthProvider, type HeapSnapshotData, type HeapSnapshotPanelData, HeapSnapshotProvider, type LogEntry, type LogLevel, type LogsData, LogsProvider, type MemoryData, MemoryProvider, type MetricsBucket, type MetricsData, MetricsProvider, type NodeUIConfig, type NodeUIMiddleware, type NodeUIOptions, type NodeUIProvider, type NodeUIServer, type OutgoingData, OutgoingProvider, type OutgoingRequestEntry, type PanelId, type ProviderContext, type ProviderError, ProviderRegistry, type ProviderResult, type RequestEntry, type RequestsData, RequestsProvider, type RequestsSummary, RingBuffer, type RouteEntry, type RouteStat, type RoutesData, RoutesProvider, SECRET_KEY_PATTERN, SECRET_MASKED, Sampler, type SamplerOptions, type SseStream, type StartupData, type StartupMark, StartupTracker, type StaticAsset, createGuard, createNodeUI, extractRoutes, hostnameFromHostHeader, interceptConsole, interceptOutgoing, isLoopbackAddress, isLoopbackHostname, maskSecretText, maskSecrets, matchesAddress, resolveActivation, resolveStaticAsset, serializeEnvelope, startSse, summarizeRequests };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
+ import { IncomingMessage, ServerResponse } from 'node:http';
1
2
  import { Readable } from 'node:stream';
2
- import { ServerResponse, IncomingMessage } from 'node:http';
3
3
 
4
4
  /**
5
5
  * Shared typed contract for the NodeUI developer console.
@@ -8,7 +8,12 @@ import { ServerResponse, IncomingMessage } from 'node:http';
8
8
  * contract. Every provider returns a {@link ProviderResult}; every REST
9
9
  * endpoint returns an {@link ApiEnvelope} which is the same shape.
10
10
  */
11
- type PanelId = 'health' | 'memory' | 'cpu' | 'event-loop' | 'heap-snapshot' | 'startup' | 'requests' | 'env' | 'routes' | 'logs' | 'metrics';
11
+ type BuiltInPanelId = 'health' | 'memory' | 'cpu' | 'event-loop' | 'heap-snapshot' | 'startup' | 'requests' | 'env' | 'routes' | 'logs' | 'metrics' | 'outgoing';
12
+ /**
13
+ * Panel identifier. Built-in panels are listed in {@link BuiltInPanelId};
14
+ * plugins may register any other string id (lowercase letters, digits, `-`).
15
+ */
16
+ type PanelId = BuiltInPanelId | (string & {});
12
17
  /** Resolved, validated console configuration. */
13
18
  interface NodeUIConfig {
14
19
  /** URL path prefix where the console and its API are served. Default `/nodeui`. */
@@ -35,6 +40,18 @@ interface NodeUIConfig {
35
40
  confirmTtlMs: number;
36
41
  /** Directory where heap snapshot files are written. */
37
42
  heapSnapshotDir: string;
43
+ /** Extra `Host` header hostnames accepted besides loopback names. */
44
+ allowedHosts: string[];
45
+ /** Extra `Origin` values accepted for cross-origin calls. */
46
+ allowedOrigins: string[];
47
+ /** Extra remote IPs / IPv4 CIDRs accepted (e.g. Docker bridge `172.17.0.0/16`). */
48
+ allowedRemoteAddresses: string[];
49
+ /** Accept requests carrying `X-Forwarded-*` headers. */
50
+ trustProxy: boolean;
51
+ /** Whether a shared access token is required. */
52
+ authRequired: boolean;
53
+ /** Maximum concurrent live (SSE) streams. */
54
+ maxSseClients: number;
38
55
  }
39
56
  /** Context handed to every provider call. */
40
57
  interface ProviderContext {
@@ -72,6 +89,8 @@ type ApiEnvelope<T> = ProviderResult<T>;
72
89
  */
73
90
  interface NodeUIProvider<T = unknown> {
74
91
  readonly id: PanelId;
92
+ /** Display label for plugin panels; defaults to the id. */
93
+ readonly title?: string;
75
94
  start?(ctx: ProviderContext): void;
76
95
  stop?(ctx: ProviderContext): void;
77
96
  get(ctx: ProviderContext): ProviderResult<T> | Promise<ProviderResult<T>>;
@@ -99,6 +118,12 @@ interface CpuData {
99
118
  totalPercent: number;
100
119
  sampleAtMs: number;
101
120
  }
121
+ interface HealthCheckResult {
122
+ name: string;
123
+ status: 'up' | 'down';
124
+ durationMs: number;
125
+ error?: string;
126
+ }
102
127
  interface HealthData {
103
128
  status: 'ok' | 'degraded' | 'critical' | 'unknown';
104
129
  statusReason: string;
@@ -108,6 +133,8 @@ interface HealthData {
108
133
  platform: string;
109
134
  eventLoopLagMs: number | null;
110
135
  memoryUsedPercent: number | null;
136
+ /** Results of user-registered dependency checks; empty when none. */
137
+ checks: HealthCheckResult[];
111
138
  }
112
139
  interface StartupMark {
113
140
  name: string;
@@ -128,9 +155,37 @@ interface RequestEntry {
128
155
  timestampMs: number;
129
156
  ip: string;
130
157
  }
158
+ interface RouteStat {
159
+ method: string;
160
+ path: string;
161
+ count: number;
162
+ avgMs: number;
163
+ p95Ms: number;
164
+ errors: number;
165
+ }
166
+ interface RequestsSummary {
167
+ /** Requests currently held in the ring buffer. */
168
+ count: number;
169
+ errors: number;
170
+ /** `errors / count` in 0..1 (5xx only). */
171
+ errorRate: number;
172
+ avgMs: number;
173
+ p50Ms: number;
174
+ p95Ms: number;
175
+ p99Ms: number;
176
+ byStatus: {
177
+ '2xx': number;
178
+ '3xx': number;
179
+ '4xx': number;
180
+ '5xx': number;
181
+ };
182
+ /** Busiest/slowest routes, slowest p95 first (at most 8). */
183
+ routes: RouteStat[];
184
+ }
131
185
  interface RequestsData {
132
186
  total: number;
133
187
  entries: RequestEntry[];
188
+ summary: RequestsSummary;
134
189
  }
135
190
  interface EnvEntry {
136
191
  key: string;
@@ -165,6 +220,21 @@ interface MetricsBucket {
165
220
  interface MetricsData {
166
221
  buckets: MetricsBucket[];
167
222
  }
223
+ interface OutgoingRequestEntry {
224
+ id: number;
225
+ method: string;
226
+ /** Target as `host/path` (query string removed). */
227
+ url: string;
228
+ status: number | null;
229
+ durationMs: number;
230
+ timestampMs: number;
231
+ error?: string;
232
+ }
233
+ interface OutgoingData {
234
+ total: number;
235
+ failed: number;
236
+ entries: OutgoingRequestEntry[];
237
+ }
168
238
  interface HeapSnapshotData {
169
239
  fileName: string;
170
240
  filePath: string;
@@ -194,6 +264,12 @@ interface ConfigData {
194
264
  enabled: boolean;
195
265
  pattern: string;
196
266
  };
267
+ locked: boolean;
268
+ /** Plugin panel ids and titles, in registration order. */
269
+ plugins: Array<{
270
+ id: PanelId;
271
+ title: string;
272
+ }>;
197
273
  }
198
274
 
199
275
  /** Replacement value used when masking secret values. */
@@ -222,6 +298,12 @@ declare class RingBuffer<T> {
222
298
 
223
299
  /** Keys whose values are redacted in any panel output. */
224
300
  declare const SECRET_KEY_PATTERN: RegExp;
301
+ /**
302
+ * Redacts secrets embedded in free text: credentials in URLs
303
+ * (`postgres://user:pass@host`), bearer/basic tokens, JWTs and
304
+ * `password=...`-style pairs. Used for log lines and any string value.
305
+ */
306
+ declare function maskSecretText(text: string): string;
225
307
  interface ActivationDecision {
226
308
  active: boolean;
227
309
  reason: string;
@@ -234,12 +316,53 @@ interface ActivationDecision {
234
316
  declare function resolveActivation(env: Record<string, string | undefined>): ActivationDecision;
235
317
  /** True when the socket address is a loopback address. */
236
318
  declare function isLoopbackAddress(address: string | undefined): boolean;
319
+ /**
320
+ * True when `address` matches one of `allowed`, each an exact IP or an IPv4
321
+ * CIDR (e.g. `172.17.0.0/16`, the default Docker bridge network).
322
+ */
323
+ declare function matchesAddress(address: string | undefined, allowed: readonly string[]): boolean;
324
+ /** Extracts the lower-cased hostname from a `Host` header value (no port). */
325
+ declare function hostnameFromHostHeader(value: string | undefined): string | null;
326
+ /** True for the hostnames a browser uses to reach a loopback server. */
327
+ declare function isLoopbackHostname(hostname: string | null): boolean;
237
328
  /**
238
329
  * Deep-clone a value, replacing any value under a key matching the secret
239
- * pattern with `[REDACTED]`. The input is not mutated.
330
+ * pattern with `[REDACTED]` and scrubbing secrets embedded in string values
331
+ * (URL credentials, bearer tokens, `password=...`). The input is not mutated.
240
332
  */
241
333
  declare function maskSecrets<T>(value: T, pattern?: RegExp): T;
242
334
 
335
+ declare const AUTH_COOKIE = "nodeui_token";
336
+ interface GuardOptions {
337
+ /** Configured console host; a loopback value enables the strict checks. */
338
+ host: string;
339
+ /** Extra `Host` header hostnames accepted (e.g. `myapp.local`). */
340
+ allowedHosts: readonly string[];
341
+ /** Extra `Origin` values accepted for cross-origin calls. */
342
+ allowedOrigins: readonly string[];
343
+ /** Extra remote IPs / IPv4 CIDRs accepted (e.g. `172.17.0.0/16` for Docker). */
344
+ allowedRemoteAddresses: readonly string[];
345
+ /** Accept requests carrying `X-Forwarded-*` headers. Default false. */
346
+ trustProxy: boolean;
347
+ /** When set, every request must present this token. */
348
+ authToken?: string;
349
+ }
350
+ type GuardResult = {
351
+ ok: true;
352
+ setToken?: string;
353
+ } | {
354
+ ok: false;
355
+ status: number;
356
+ code: string;
357
+ message: string;
358
+ };
359
+ /**
360
+ * Builds the request guard for the console. It combines: loopback/remote
361
+ * address policy, `Host` validation (DNS-rebinding defence), `Origin`
362
+ * validation (cross-site request defence) and an optional shared token.
363
+ */
364
+ declare function createGuard(options: GuardOptions): (req: IncomingMessage) => GuardResult;
365
+
243
366
  /**
244
367
  * Single-use, expiring confirmation nonces. Mutating actions (such as heap
245
368
  * snapshots) require a nonce issued here, presented via the
@@ -247,8 +370,9 @@ declare function maskSecrets<T>(value: T, pattern?: RegExp): T;
247
370
  */
248
371
  declare class ConfirmationStore {
249
372
  private readonly ttlMs;
373
+ private readonly maxOutstanding;
250
374
  private nonces;
251
- constructor(ttlMs: number);
375
+ constructor(ttlMs: number, maxOutstanding?: number);
252
376
  issue(): ConfirmIssued;
253
377
  /** Consumes `nonce` and returns true only if it was valid and unexpired. */
254
378
  consume(nonce: string): boolean;
@@ -381,12 +505,45 @@ declare class EventLoopLagProvider implements NodeUIProvider<EventLoopSample> {
381
505
  };
382
506
  }
383
507
 
384
- /** Derives process + event-loop health from the latest sampled values. */
508
+ /**
509
+ * A dependency check (database ping, cache ping, ...). Resolve for healthy,
510
+ * reject/throw (or resolve `false`) for unhealthy.
511
+ */
512
+ type HealthCheck = () => unknown | Promise<unknown>;
513
+ /** Derives process + event-loop health, plus optional dependency checks. */
385
514
  declare class HealthProvider implements NodeUIProvider<HealthData> {
515
+ private readonly checks;
386
516
  readonly id: "health";
387
- get(ctx: ProviderContext): {
517
+ private cache;
518
+ constructor(checks?: Record<string, HealthCheck>);
519
+ private runChecks;
520
+ get(ctx: ProviderContext): Promise<{
388
521
  ok: true;
389
522
  data: HealthData;
523
+ }>;
524
+ }
525
+
526
+ type Recorder = (entry: Omit<OutgoingRequestEntry, 'id'>) => void;
527
+ /**
528
+ * Refcounted instrumentation of outgoing HTTP calls made via `http`,
529
+ * `https` (axios, got, node-fetch, ...) and global `fetch` (undici). The
530
+ * first consumer installs the hooks, the last release restores everything.
531
+ */
532
+ declare function interceptOutgoing(record: Recorder): () => void;
533
+ /** Recent outgoing HTTP calls, recorded lazily while the panel is active. */
534
+ declare class OutgoingProvider implements NodeUIProvider<OutgoingData> {
535
+ readonly id: "outgoing";
536
+ private buffer;
537
+ private nextId;
538
+ private failed;
539
+ private release;
540
+ constructor(size: number);
541
+ start(): void;
542
+ stop(): void;
543
+ record(entry: Omit<OutgoingRequestEntry, 'id'>): void;
544
+ get(): {
545
+ ok: true;
546
+ data: OutgoingData;
390
547
  };
391
548
  }
392
549
 
@@ -405,6 +562,7 @@ declare class HeapSnapshotProvider implements NodeUIProvider<HeapSnapshotPanelDa
405
562
  takeSnapshot(ctx: ProviderContext): Promise<ProviderResult<HeapSnapshotData>>;
406
563
  }
407
564
 
565
+ declare function summarizeRequests(entries: readonly RequestEntry[]): RequestsSummary;
408
566
  /** Fixed-size ring buffer of recent HTTP requests, recorded via middleware hook. */
409
567
  declare class RequestsProvider implements NodeUIProvider<RequestsData> {
410
568
  readonly id: "requests";
@@ -521,7 +679,7 @@ interface SseStream {
521
679
  * Minimal Server-Sent Events writer over a raw HTTP response. Events are
522
680
  * emitted as `data: <json>\n\n`; heartbeats as `:ping\n\n`.
523
681
  */
524
- declare function startSse(res: ServerResponse): SseStream;
682
+ declare function startSse(res: ServerResponse, extraHeaders?: Record<string, string>): SseStream;
525
683
 
526
684
  interface NodeUIOptions {
527
685
  /** URL path prefix. Default `/nodeui` (or `NODEUI_PATH`). */
@@ -548,8 +706,32 @@ interface NodeUIOptions {
548
706
  inactivityTimeoutMs?: number;
549
707
  /** TTL for mutation confirmation nonces. Default 60000. */
550
708
  confirmTtlMs?: number;
551
- /** Directory for heap snapshot files. Defaults to the OS temp dir. */
709
+ /** Directory for heap snapshot files. Defaults to a private dir under the OS temp dir. */
552
710
  heapSnapshotDir?: string;
711
+ /** Extra `Host` hostnames accepted besides loopback names (or `NODEUI_ALLOWED_HOSTS`, comma-separated). */
712
+ allowedHosts?: string[];
713
+ /** Extra `Origin` values accepted for cross-origin calls (or `NODEUI_ALLOWED_ORIGINS`). */
714
+ allowedOrigins?: string[];
715
+ /**
716
+ * Extra remote IPs / IPv4 CIDRs accepted (or `NODEUI_ALLOWED_REMOTE`). Use
717
+ * `['172.16.0.0/12']` to reach the console from the host when the app runs in Docker.
718
+ */
719
+ allowedRemoteAddresses?: string[];
720
+ /** Accept requests carrying `X-Forwarded-*` headers (or `NODEUI_TRUST_PROXY=true`). Default false. */
721
+ trustProxy?: boolean;
722
+ /**
723
+ * Shared access token (or `NODEUI_TOKEN`). When set, open `/nodeui/?token=<token>` once;
724
+ * the browser then keeps an HttpOnly cookie. API clients may send `Authorization: Bearer`.
725
+ */
726
+ authToken?: string;
727
+ /** Maximum concurrent live (SSE) streams. Default 10. */
728
+ maxSseClients?: number;
729
+ /** Custom panels; each provider needs a unique lowercase id, e.g. `queues`. */
730
+ plugins?: NodeUIProvider[];
731
+ /** Named dependency checks (database, cache, ...) surfaced in the health panel. */
732
+ healthChecks?: Record<string, HealthCheck>;
733
+ /** Max recorded outgoing HTTP calls. Default 200. */
734
+ outgoingLogSize?: number;
553
735
  }
554
736
  interface NodeUIServer {
555
737
  readonly config: NodeUIConfig;
@@ -559,6 +741,11 @@ interface NodeUIServer {
559
741
  middleware(): NodeUIMiddleware;
560
742
  /** Direct request handling (no `next`); used for tests and adapters. */
561
743
  handle(req: IncomingMessage, res: ServerResponse): Promise<void>;
744
+ /**
745
+ * Supplies the route list directly (frameworks other than Express, e.g.
746
+ * Fastify's `onRoute`). Takes precedence over Express router introspection.
747
+ */
748
+ setRoutes(source: RouteEntry[] | (() => RouteEntry[])): void;
562
749
  /** Records a bootstrap timing mark, e.g. `mark("listening")`. */
563
750
  mark(name: string): void;
564
751
  /** Whether a background-sampling provider is currently running. */
@@ -576,7 +763,7 @@ type NodeUIMiddleware = (req: IncomingMessage, res: ServerResponse, next: () =>
576
763
  * Serializes an API envelope to JSON, applying secret masking so values
577
764
  * under keys like `TOKEN`/`KEY`/`SECRET`/`PASSWORD` never reach the UI.
578
765
  */
579
- declare function serializeEnvelope<T>(envelope: ApiEnvelope<T>): string;
766
+ declare function serializeEnvelope<T>(envelope: ApiEnvelope<T>, mask?: boolean): string;
580
767
  declare function createNodeUI(options?: NodeUIOptions): NodeUIServer;
581
768
 
582
- export { type ActivationDecision, type ApiEnvelope, type ConfigData, type ConfirmIssued, ConfirmationStore, type CpuClock, type CpuData, CpuProvider, type CpuUsageSnapshot, DEFAULT_LOG_SIZE, type EnvData, type EnvEntry, EnvProvider, type EventLoopClock, EventLoopLagProvider, type EventLoopSample, type HealthData, HealthProvider, type HeapSnapshotData, type HeapSnapshotPanelData, HeapSnapshotProvider, type LogEntry, type LogLevel, type LogsData, LogsProvider, type MemoryData, MemoryProvider, type MetricsBucket, type MetricsData, MetricsProvider, type NodeUIConfig, type NodeUIMiddleware, type NodeUIOptions, type NodeUIProvider, type NodeUIServer, type PanelId, type ProviderContext, type ProviderError, ProviderRegistry, type ProviderResult, type RequestEntry, type RequestsData, RequestsProvider, RingBuffer, type RouteEntry, type RoutesData, RoutesProvider, SECRET_KEY_PATTERN, SECRET_MASKED, Sampler, type SamplerOptions, type SseStream, type StartupData, type StartupMark, StartupTracker, type StaticAsset, createNodeUI, extractRoutes, interceptConsole, isLoopbackAddress, maskSecrets, resolveActivation, resolveStaticAsset, serializeEnvelope, startSse };
769
+ export { AUTH_COOKIE, type ActivationDecision, type ApiEnvelope, type BuiltInPanelId, type ConfigData, type ConfirmIssued, ConfirmationStore, type CpuClock, type CpuData, CpuProvider, type CpuUsageSnapshot, DEFAULT_LOG_SIZE, type EnvData, type EnvEntry, EnvProvider, type EventLoopClock, EventLoopLagProvider, type EventLoopSample, type GuardOptions, type GuardResult, type HealthCheck, type HealthCheckResult, type HealthData, HealthProvider, type HeapSnapshotData, type HeapSnapshotPanelData, HeapSnapshotProvider, type LogEntry, type LogLevel, type LogsData, LogsProvider, type MemoryData, MemoryProvider, type MetricsBucket, type MetricsData, MetricsProvider, type NodeUIConfig, type NodeUIMiddleware, type NodeUIOptions, type NodeUIProvider, type NodeUIServer, type OutgoingData, OutgoingProvider, type OutgoingRequestEntry, type PanelId, type ProviderContext, type ProviderError, ProviderRegistry, type ProviderResult, type RequestEntry, type RequestsData, RequestsProvider, type RequestsSummary, RingBuffer, type RouteEntry, type RouteStat, type RoutesData, RoutesProvider, SECRET_KEY_PATTERN, SECRET_MASKED, Sampler, type SamplerOptions, type SseStream, type StartupData, type StartupMark, StartupTracker, type StaticAsset, createGuard, createNodeUI, extractRoutes, hostnameFromHostHeader, interceptConsole, interceptOutgoing, isLoopbackAddress, isLoopbackHostname, maskSecretText, maskSecrets, matchesAddress, resolveActivation, resolveStaticAsset, serializeEnvelope, startSse, summarizeRequests };