@singhak/nodeui-core 0.3.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/README.md CHANGED
@@ -1,111 +1,111 @@
1
- # @singhak/nodeui-core
2
-
3
- Framework-neutral engine for the NodeUI developer console: observability
4
- providers, the REST contract, the safety gate, and the static console server.
5
- The Express, Fastify and NestJS adapters build on this package; you normally
6
- consume those instead of using `@singhak/nodeui-core` directly. Use core
7
- directly to integrate a framework without an adapter (Koa, Hono, plain `http`).
8
-
9
- ## Providers
10
-
11
- Each provider exposes one panel through `GET {path}/api/{id}`:
12
-
13
- | id | Panel | Notes |
14
- | --------------- | -------------- | ---------------------------------------------------------- |
15
- | `health` | Health | Event-loop + memory status, plus your `healthChecks`. |
16
- | `memory` | Memory | Samples process + OS memory on start. |
17
- | `cpu` | CPU | Process CPU percent from `process.cpuUsage()`. |
18
- | `event-loop` | Event-loop lag | Drift vs. a 1ms `setInterval`, last 600 samples. |
19
- | `heap-snapshot` | Heap snapshot | Writes `nodeui-heap-<pid>-<ts>.heapsnapshot` (owner-only). |
20
- | `startup` | Startup | Timing marks recorded via `server.mark(name)`. |
21
- | `requests` | Requests | Ring buffer of app HTTP requests (default 500). |
22
- | `metrics` | Metrics | Requests/errors per second for the last minute. |
23
- | `env` | Environment | `process.env` + your `config`, masked. |
24
- | `routes` | Routes | Express router introspection, or `server.setRoutes()`. |
25
- | `logs` | Logs | `console.*` capture plus `server.addLogSource()`. |
26
- | `outgoing` | Outgoing HTTP | `http`/`https`/`fetch` calls made by the app. |
27
- | _your id_ | _your title_ | Custom panels via the `plugins` option (see below). |
28
-
29
- ## API
30
-
31
- All endpoints live under `{path}/api` and return
32
- `{ ok: true, data } | { ok: false, error: { code, message } }`:
33
-
34
- - `GET /config` — effective configuration, panel list and plugin titles.
35
- - `GET /{panel-id}` — panel data for every id above.
36
- - `GET /live?panels=a,b` — Server-Sent Events stream (capped by `maxSseClients`).
37
- - `POST /confirmations` — issue a single-use nonce.
38
- - `POST /heap-snapshot` — capture a snapshot; must send
39
- `x-nodeui-confirm: <nonce>` from a fresh confirmation. A missing, expired, or
40
- replayed nonce returns `409` with error code `confirmation-required`.
41
-
42
- `GET /` serves the bundled React console with an SPA fallback to `index.html`.
43
-
44
- ## Safety
45
-
46
- - **Activation:** active when `NODE_ENV` is non-production, or when
47
- `NODEUI_ENABLED=true` forces it; fail-closed in production (a warning is logged if forced on).
48
- - **Loopback-only:** remote hosts are rejected with `403`. Extra clients can be allow-listed
49
- (`allowedRemoteAddresses`, exact IPs or IPv4 CIDRs, e.g. the Docker bridge).
50
- - **Host / Origin validation:** blocks DNS-rebinding and cross-site requests. Extra names via
51
- `allowedHosts` / `allowedOrigins`. `X-Forwarded-*` requests are rejected unless `trustProxy`.
52
- - **Access token (optional):** `authToken` / `NODEUI_TOKEN`; send `Authorization: Bearer <token>`,
53
- or open `{path}/?token=<token>` once to get an HttpOnly cookie.
54
- - **Secret masking:** values under keys such as `token`, `key`, `secret`, `password`, `auth`,
55
- `cookie`, `dsn`, `database_url` are replaced with `[REDACTED]`; credentials inside text
56
- (URLs, bearer tokens, JWTs, `password=...`) are scrubbed too, in REST and SSE. Disable with
57
- `maskSecrets: false`.
58
- - **Headers:** CSP, `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy` on every response.
59
- - **Read-only default:** the heap snapshot is the only mutating action and is
60
- gated behind a fresh nonce.
61
- - **Zero-cost when disabled:** the middleware is a pass-through; no timers or
62
- hooks are created.
63
-
64
- ## Configuration
65
-
66
- Environment variables are read at `createNodeUI()` time (overridable via the
67
- `options.env` map for tests); each also has a typed option of the same meaning.
68
-
69
- | Variable | Default | Description |
70
- | ------------------------------ | ------------------- | ---------------------------------------- |
71
- | `NODEUI_ENABLED` | unset | Force activation (`true`) / disable. |
72
- | `NODEUI_HOST` | `127.0.0.1` | Loopback host the console trusts. |
73
- | `NODEUI_PATH` | `/nodeui` | URL prefix for console + API. |
74
- | `NODEUI_REQUEST_LOG_SIZE` | `500` | Request ring-buffer capacity. |
75
- | `NODEUI_LOG_SIZE` | `500` | Log ring-buffer capacity. |
76
- | `NODEUI_POLL_INTERVAL_MS` | `2000` | Sampling interval. |
77
- | `NODEUI_INACTIVITY_TIMEOUT_MS` | `60000` | Provider idle timeout. |
78
- | `NODEUI_CONFIRM_TTL_MS` | `60000` | Nonce lifetime. |
79
- | `NODEUI_HEAP_SNAPSHOT_DIR` | `<tmp>/nodeui-heap` | Snapshot output directory. |
80
- | `NODEUI_TOKEN` | unset | Require an access token. |
81
- | `NODEUI_ALLOWED_HOSTS` | unset | Comma-separated extra `Host` names. |
82
- | `NODEUI_ALLOWED_ORIGINS` | unset | Comma-separated extra `Origin` values. |
83
- | `NODEUI_ALLOWED_REMOTE` | unset | Comma-separated client IPs / IPv4 CIDRs. |
84
- | `NODEUI_TRUST_PROXY` | `false` | Accept `X-Forwarded-*` requests. |
85
- | `NODEUI_MAX_SSE_CLIENTS` | `10` | Concurrent live streams. |
86
-
87
- ## Usage
88
-
89
- ```ts
90
- import { createNodeUI, type NodeUIServer } from '@singhak/nodeui-core';
91
-
92
- const server: NodeUIServer = createNodeUI({
93
- plugins: [{ id: 'queues', title: 'Queues', get: async () => ({ ok: true, data: await jobs() }) }],
94
- healthChecks: { db: () => pool.query('select 1') },
95
- });
96
- const middleware = server.middleware();
97
- // middleware is (req, res, next) => void; mount it before your routes.
98
- server.mark('listening');
99
- server.shutdown();
100
- ```
101
-
102
- Plugin ids are lowercase letters, digits and `-`, and must not collide with a built-in panel.
103
- Without an Express router, feed the routes panel with `server.setRoutes([...])`.
104
-
105
- ## Development
106
-
107
- ```bash
108
- npm run typecheck # tsc --noEmit
109
- npm test # vitest run (unit + e2e)
110
- npm run build # emit dist/
111
- ```
1
+ # @singhak/nodeui-core
2
+
3
+ Framework-neutral engine for the NodeUI developer console: observability
4
+ providers, the REST contract, the safety gate, and the static console server.
5
+ The Express, Fastify and NestJS adapters build on this package; you normally
6
+ consume those instead of using `@singhak/nodeui-core` directly. Use core
7
+ directly to integrate a framework without an adapter (Koa, Hono, plain `http`).
8
+
9
+ ## Providers
10
+
11
+ Each provider exposes one panel through `GET {path}/api/{id}`:
12
+
13
+ | id | Panel | Notes |
14
+ | --------------- | -------------- | ---------------------------------------------------------- |
15
+ | `health` | Health | Event-loop + memory status, plus your `healthChecks`. |
16
+ | `memory` | Memory | Samples process + OS memory on start. |
17
+ | `cpu` | CPU | Process CPU percent from `process.cpuUsage()`. |
18
+ | `event-loop` | Event-loop lag | Drift vs. a 1ms `setInterval`, last 600 samples. |
19
+ | `heap-snapshot` | Heap snapshot | Writes `nodeui-heap-<pid>-<ts>.heapsnapshot` (owner-only). |
20
+ | `startup` | Startup | Timing marks recorded via `server.mark(name)`. |
21
+ | `requests` | Requests | Ring buffer of app HTTP requests (default 500). |
22
+ | `metrics` | Metrics | Requests/errors per second for the last minute. |
23
+ | `env` | Environment | `process.env` + your `config`, masked. |
24
+ | `routes` | Routes | Express router introspection, or `server.setRoutes()`. |
25
+ | `logs` | Logs | `console.*` capture plus `server.addLogSource()`. |
26
+ | `outgoing` | Outgoing HTTP | `http`/`https`/`fetch` calls made by the app. |
27
+ | _your id_ | _your title_ | Custom panels via the `plugins` option (see below). |
28
+
29
+ ## API
30
+
31
+ All endpoints live under `{path}/api` and return
32
+ `{ ok: true, data } | { ok: false, error: { code, message } }`:
33
+
34
+ - `GET /config` — effective configuration, panel list and plugin titles.
35
+ - `GET /{panel-id}` — panel data for every id above.
36
+ - `GET /live?panels=a,b` — Server-Sent Events stream (capped by `maxSseClients`).
37
+ - `POST /confirmations` — issue a single-use nonce.
38
+ - `POST /heap-snapshot` — capture a snapshot; must send
39
+ `x-nodeui-confirm: <nonce>` from a fresh confirmation. A missing, expired, or
40
+ replayed nonce returns `409` with error code `confirmation-required`.
41
+
42
+ `GET /` serves the bundled React console with an SPA fallback to `index.html`.
43
+
44
+ ## Safety
45
+
46
+ - **Activation:** active when `NODE_ENV` is non-production, or when
47
+ `NODEUI_ENABLED=true` forces it; fail-closed in production (a warning is logged if forced on).
48
+ - **Loopback-only:** remote hosts are rejected with `403`. Extra clients can be allow-listed
49
+ (`allowedRemoteAddresses`, exact IPs or IPv4 CIDRs, e.g. the Docker bridge).
50
+ - **Host / Origin validation:** blocks DNS-rebinding and cross-site requests. Extra names via
51
+ `allowedHosts` / `allowedOrigins`. `X-Forwarded-*` requests are rejected unless `trustProxy`.
52
+ - **Access token (optional):** `authToken` / `NODEUI_TOKEN`; send `Authorization: Bearer <token>`,
53
+ or open `{path}/?token=<token>` once to get an HttpOnly cookie.
54
+ - **Secret masking:** values under keys such as `token`, `key`, `secret`, `password`, `auth`,
55
+ `cookie`, `dsn`, `database_url` are replaced with `[REDACTED]`; credentials inside text
56
+ (URLs, bearer tokens, JWTs, `password=...`) are scrubbed too, in REST and SSE. Disable with
57
+ `maskSecrets: false`.
58
+ - **Headers:** CSP, `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy` on every response.
59
+ - **Read-only default:** the heap snapshot is the only mutating action and is
60
+ gated behind a fresh nonce.
61
+ - **Zero-cost when disabled:** the middleware is a pass-through; no timers or
62
+ hooks are created.
63
+
64
+ ## Configuration
65
+
66
+ Environment variables are read at `createNodeUI()` time (overridable via the
67
+ `options.env` map for tests); each also has a typed option of the same meaning.
68
+
69
+ | Variable | Default | Description |
70
+ | ------------------------------ | ------------------- | ---------------------------------------- |
71
+ | `NODEUI_ENABLED` | unset | Force activation (`true`) / disable. |
72
+ | `NODEUI_HOST` | `127.0.0.1` | Loopback host the console trusts. |
73
+ | `NODEUI_PATH` | `/nodeui` | URL prefix for console + API. |
74
+ | `NODEUI_REQUEST_LOG_SIZE` | `500` | Request ring-buffer capacity. |
75
+ | `NODEUI_LOG_SIZE` | `500` | Log ring-buffer capacity. |
76
+ | `NODEUI_POLL_INTERVAL_MS` | `2000` | Sampling interval. |
77
+ | `NODEUI_INACTIVITY_TIMEOUT_MS` | `60000` | Provider idle timeout. |
78
+ | `NODEUI_CONFIRM_TTL_MS` | `60000` | Nonce lifetime. |
79
+ | `NODEUI_HEAP_SNAPSHOT_DIR` | `<tmp>/nodeui-heap` | Snapshot output directory. |
80
+ | `NODEUI_TOKEN` | unset | Require an access token. |
81
+ | `NODEUI_ALLOWED_HOSTS` | unset | Comma-separated extra `Host` names. |
82
+ | `NODEUI_ALLOWED_ORIGINS` | unset | Comma-separated extra `Origin` values. |
83
+ | `NODEUI_ALLOWED_REMOTE` | unset | Comma-separated client IPs / IPv4 CIDRs. |
84
+ | `NODEUI_TRUST_PROXY` | `false` | Accept `X-Forwarded-*` requests. |
85
+ | `NODEUI_MAX_SSE_CLIENTS` | `10` | Concurrent live streams. |
86
+
87
+ ## Usage
88
+
89
+ ```ts
90
+ import { createNodeUI, type NodeUIServer } from '@singhak/nodeui-core';
91
+
92
+ const server: NodeUIServer = createNodeUI({
93
+ plugins: [{ id: 'queues', title: 'Queues', get: async () => ({ ok: true, data: await jobs() }) }],
94
+ healthChecks: { db: () => pool.query('select 1') },
95
+ });
96
+ const middleware = server.middleware();
97
+ // middleware is (req, res, next) => void; mount it before your routes.
98
+ server.mark('listening');
99
+ server.shutdown();
100
+ ```
101
+
102
+ Plugin ids are lowercase letters, digits and `-`, and must not collide with a built-in panel.
103
+ Without an Express router, feed the routes panel with `server.setRoutes([...])`.
104
+
105
+ ## Development
106
+
107
+ ```bash
108
+ npm run typecheck # tsc --noEmit
109
+ npm test # vitest run (unit + e2e)
110
+ npm run build # emit dist/
111
+ ```
package/dist/index.cjs CHANGED
@@ -64,7 +64,8 @@ __export(index_exports, {
64
64
  resolveActivation: () => resolveActivation,
65
65
  resolveStaticAsset: () => resolveStaticAsset,
66
66
  serializeEnvelope: () => serializeEnvelope,
67
- startSse: () => startSse
67
+ startSse: () => startSse,
68
+ summarizeRequests: () => summarizeRequests
68
69
  });
69
70
  module.exports = __toCommonJS(index_exports);
70
71
 
@@ -978,6 +979,54 @@ var HeapSnapshotProvider = class {
978
979
 
979
980
  // src/providers/requests.ts
980
981
  var RESPONSE_PAGE_SIZE2 = 100;
982
+ var MAX_ROUTE_STATS = 8;
983
+ function percentile(sorted, p) {
984
+ if (sorted.length === 0) return 0;
985
+ const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(p * sorted.length) - 1));
986
+ return sorted[index] ?? 0;
987
+ }
988
+ function routeKey(path) {
989
+ return path.split("/").map(
990
+ (seg) => /^\d+$/.test(seg) || /^[0-9a-f]{8}-[0-9a-f-]{27}$/i.test(seg) || /^[0-9a-f]{24}$/i.test(seg) ? ":id" : seg
991
+ ).join("/");
992
+ }
993
+ function summarizeRequests(entries) {
994
+ const durations = entries.map((e) => e.durationMs).sort((a, b) => a - b);
995
+ const byStatus = { "2xx": 0, "3xx": 0, "4xx": 0, "5xx": 0 };
996
+ const groups = /* @__PURE__ */ new Map();
997
+ for (const e of entries) {
998
+ const bucket = `${Math.floor(e.status / 100)}xx`;
999
+ if (bucket in byStatus) byStatus[bucket] += 1;
1000
+ const path = routeKey(e.path);
1001
+ const key = `${e.method} ${path}`;
1002
+ const group = groups.get(key) ?? { method: e.method, path, ms: [], errors: 0 };
1003
+ group.ms.push(e.durationMs);
1004
+ if (e.status >= 500) group.errors += 1;
1005
+ groups.set(key, group);
1006
+ }
1007
+ const routes = [...groups.values()].map((g) => {
1008
+ const sorted = [...g.ms].sort((a, b) => a - b);
1009
+ return {
1010
+ method: g.method,
1011
+ path: g.path,
1012
+ count: g.ms.length,
1013
+ avgMs: g.ms.reduce((a, b) => a + b, 0) / g.ms.length,
1014
+ p95Ms: percentile(sorted, 0.95),
1015
+ errors: g.errors
1016
+ };
1017
+ }).sort((a, b) => b.p95Ms - a.p95Ms).slice(0, MAX_ROUTE_STATS);
1018
+ return {
1019
+ count: entries.length,
1020
+ errors: byStatus["5xx"],
1021
+ errorRate: entries.length === 0 ? 0 : byStatus["5xx"] / entries.length,
1022
+ avgMs: durations.length === 0 ? 0 : durations.reduce((a, b) => a + b, 0) / durations.length,
1023
+ p50Ms: percentile(durations, 0.5),
1024
+ p95Ms: percentile(durations, 0.95),
1025
+ p99Ms: percentile(durations, 0.99),
1026
+ byStatus,
1027
+ routes
1028
+ };
1029
+ }
981
1030
  var RequestsProvider = class {
982
1031
  id = "requests";
983
1032
  buffer;
@@ -994,7 +1043,8 @@ var RequestsProvider = class {
994
1043
  }
995
1044
  get() {
996
1045
  const entries = this.buffer.slice(Math.max(0, this.buffer.length - RESPONSE_PAGE_SIZE2));
997
- return { ok: true, data: { total: this.buffer.length, entries } };
1046
+ const summary = summarizeRequests(this.buffer.toArray());
1047
+ return { ok: true, data: { total: this.buffer.length, entries, summary } };
998
1048
  }
999
1049
  };
1000
1050
 
@@ -1516,7 +1566,7 @@ function createNodeUI(options = {}) {
1516
1566
  pollIntervalMs: config.pollIntervalMs,
1517
1567
  panels: registry.ids(),
1518
1568
  masking: { enabled: config.maskSecrets, pattern: SECRET_KEY_PATTERN.source },
1519
- authRequired: config.authRequired,
1569
+ locked: config.authRequired,
1520
1570
  plugins: pluginMeta
1521
1571
  };
1522
1572
  }
@@ -1786,6 +1836,7 @@ function createNodeUI(options = {}) {
1786
1836
  resolveActivation,
1787
1837
  resolveStaticAsset,
1788
1838
  serializeEnvelope,
1789
- startSse
1839
+ startSse,
1840
+ summarizeRequests
1790
1841
  });
1791
1842
  //# sourceMappingURL=index.cjs.map