@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 +111 -111
- package/dist/index.cjs +55 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +31 -2
- package/dist/index.d.ts +31 -2
- package/dist/index.mjs +53 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/static/assets/index-Cu0xtlkD.js +11 -0
- package/static/assets/index-DlUszrsn.css +1 -0
- package/static/index.html +14 -13
- package/static/assets/index-CJCpPtrr.js +0 -11
- package/static/assets/index-ChbLMdDF.css +0 -1
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
|
-
|
|
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
|
-
|
|
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
|