@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/README.md +59 -30
- package/dist/index.cjs +652 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +197 -10
- package/dist/index.d.ts +197 -10
- package/dist/index.mjs +632 -49
- package/dist/index.mjs.map +1 -1
- package/package.json +63 -63
- 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-DbLC8b3x.css +0 -1
- package/static/assets/index-DowJtJYZ.js +0 -10
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,11 +17,20 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
21
31
|
var index_exports = {};
|
|
22
32
|
__export(index_exports, {
|
|
33
|
+
AUTH_COOKIE: () => AUTH_COOKIE,
|
|
23
34
|
ConfirmationStore: () => ConfirmationStore,
|
|
24
35
|
CpuProvider: () => CpuProvider,
|
|
25
36
|
DEFAULT_LOG_SIZE: () => DEFAULT_LOG_SIZE,
|
|
@@ -30,6 +41,7 @@ __export(index_exports, {
|
|
|
30
41
|
LogsProvider: () => LogsProvider,
|
|
31
42
|
MemoryProvider: () => MemoryProvider,
|
|
32
43
|
MetricsProvider: () => MetricsProvider,
|
|
44
|
+
OutgoingProvider: () => OutgoingProvider,
|
|
33
45
|
ProviderRegistry: () => ProviderRegistry,
|
|
34
46
|
RequestsProvider: () => RequestsProvider,
|
|
35
47
|
RingBuffer: () => RingBuffer,
|
|
@@ -38,15 +50,22 @@ __export(index_exports, {
|
|
|
38
50
|
SECRET_MASKED: () => SECRET_MASKED,
|
|
39
51
|
Sampler: () => Sampler,
|
|
40
52
|
StartupTracker: () => StartupTracker,
|
|
53
|
+
createGuard: () => createGuard,
|
|
41
54
|
createNodeUI: () => createNodeUI,
|
|
42
55
|
extractRoutes: () => extractRoutes,
|
|
56
|
+
hostnameFromHostHeader: () => hostnameFromHostHeader,
|
|
43
57
|
interceptConsole: () => interceptConsole,
|
|
58
|
+
interceptOutgoing: () => interceptOutgoing,
|
|
44
59
|
isLoopbackAddress: () => isLoopbackAddress,
|
|
60
|
+
isLoopbackHostname: () => isLoopbackHostname,
|
|
61
|
+
maskSecretText: () => maskSecretText,
|
|
45
62
|
maskSecrets: () => maskSecrets,
|
|
63
|
+
matchesAddress: () => matchesAddress,
|
|
46
64
|
resolveActivation: () => resolveActivation,
|
|
47
65
|
resolveStaticAsset: () => resolveStaticAsset,
|
|
48
66
|
serializeEnvelope: () => serializeEnvelope,
|
|
49
|
-
startSse: () => startSse
|
|
67
|
+
startSse: () => startSse,
|
|
68
|
+
summarizeRequests: () => summarizeRequests
|
|
50
69
|
});
|
|
51
70
|
module.exports = __toCommonJS(index_exports);
|
|
52
71
|
|
|
@@ -95,7 +114,15 @@ var RingBuffer = class {
|
|
|
95
114
|
};
|
|
96
115
|
|
|
97
116
|
// src/safety.ts
|
|
98
|
-
var SECRET_KEY_PATTERN = /token|key|secret|
|
|
117
|
+
var SECRET_KEY_PATTERN = /token|key|secret|passw(?:or)?d|credential|auth|cookie|dsn|private|signature|session|bearer|connection.?string|database.?url|conn.?str/i;
|
|
118
|
+
var TEXT_PAIR_PATTERN = /\b([\w.-]*(?:token|secret|passw(?:or)?d|api[_-]?key|credential|authorization|cookie|dsn)[\w.-]*["']?)(\s*[=:]\s*)("[^"]*"|'[^']*'|[^\s,;&"'}]+)/gi;
|
|
119
|
+
var URL_CREDENTIALS_PATTERN = /([a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:)[^\s@/]+(@)/gi;
|
|
120
|
+
var BEARER_PATTERN = /\b(Bearer|Basic)\s+[A-Za-z0-9\-._~+/]+=*/gi;
|
|
121
|
+
var JWT_PATTERN = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]*/g;
|
|
122
|
+
function maskSecretText(text) {
|
|
123
|
+
if (text.length < 6) return text;
|
|
124
|
+
return text.replace(URL_CREDENTIALS_PATTERN, `$1${SECRET_MASKED}$2`).replace(BEARER_PATTERN, `$1 ${SECRET_MASKED}`).replace(JWT_PATTERN, SECRET_MASKED).replace(TEXT_PAIR_PATTERN, `$1$2${SECRET_MASKED}`);
|
|
125
|
+
}
|
|
99
126
|
function resolveActivation(env) {
|
|
100
127
|
if (env.NODEUI_ENABLED === "true") {
|
|
101
128
|
return { active: true, reason: "activated by NODEUI_ENABLED=true" };
|
|
@@ -118,7 +145,55 @@ function isLoopbackAddress(address) {
|
|
|
118
145
|
if (!address) return false;
|
|
119
146
|
return address.startsWith("127.") || address === "::1" || address.startsWith("::ffff:127.") || address === "localhost";
|
|
120
147
|
}
|
|
148
|
+
function normalizeIp(address) {
|
|
149
|
+
return address.startsWith("::ffff:") ? address.slice(7) : address;
|
|
150
|
+
}
|
|
151
|
+
function ipv4ToInt(ip) {
|
|
152
|
+
const parts = ip.split(".");
|
|
153
|
+
if (parts.length !== 4) return null;
|
|
154
|
+
let out = 0;
|
|
155
|
+
for (const part of parts) {
|
|
156
|
+
const n = Number(part);
|
|
157
|
+
if (!Number.isInteger(n) || n < 0 || n > 255) return null;
|
|
158
|
+
out = out * 256 + n;
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
function matchesAddress(address, allowed) {
|
|
163
|
+
if (!address) return false;
|
|
164
|
+
const ip = normalizeIp(address);
|
|
165
|
+
for (const entry of allowed) {
|
|
166
|
+
if (!entry.includes("/")) {
|
|
167
|
+
if (normalizeIp(entry) === ip) return true;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const [base, bitsRaw] = entry.split("/");
|
|
171
|
+
const bits = Number(bitsRaw);
|
|
172
|
+
const a = ipv4ToInt(ip);
|
|
173
|
+
const b = ipv4ToInt(base ?? "");
|
|
174
|
+
if (a === null || b === null || !Number.isInteger(bits) || bits < 0 || bits > 32) continue;
|
|
175
|
+
const mask = bits === 0 ? 0 : 4294967295 << 32 - bits >>> 0;
|
|
176
|
+
if ((a & mask) >>> 0 === (b & mask) >>> 0) return true;
|
|
177
|
+
}
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
function hostnameFromHostHeader(value) {
|
|
181
|
+
if (!value) return null;
|
|
182
|
+
const trimmed = value.trim().toLowerCase();
|
|
183
|
+
if (trimmed.startsWith("[")) {
|
|
184
|
+
const end = trimmed.indexOf("]");
|
|
185
|
+
return end === -1 ? null : trimmed.slice(1, end);
|
|
186
|
+
}
|
|
187
|
+
return trimmed.split(":")[0] || null;
|
|
188
|
+
}
|
|
189
|
+
function isLoopbackHostname(hostname) {
|
|
190
|
+
if (!hostname) return false;
|
|
191
|
+
return hostname === "localhost" || hostname.endsWith(".localhost") || isLoopbackAddress(hostname);
|
|
192
|
+
}
|
|
121
193
|
function maskSecrets(value, pattern = SECRET_KEY_PATTERN) {
|
|
194
|
+
if (typeof value === "string") {
|
|
195
|
+
return maskSecretText(value);
|
|
196
|
+
}
|
|
122
197
|
if (value !== null && typeof value === "object") {
|
|
123
198
|
if (Array.isArray(value)) {
|
|
124
199
|
return value.map((v) => maskSecrets(v, pattern));
|
|
@@ -142,17 +217,133 @@ function maskSecrets(value, pattern = SECRET_KEY_PATTERN) {
|
|
|
142
217
|
return value;
|
|
143
218
|
}
|
|
144
219
|
|
|
145
|
-
// src/
|
|
220
|
+
// src/guard.ts
|
|
146
221
|
var import_node_crypto = require("crypto");
|
|
222
|
+
var AUTH_COOKIE = "nodeui_token";
|
|
223
|
+
var FORWARD_HEADERS = ["x-forwarded-for", "x-forwarded-host", "forwarded", "x-real-ip"];
|
|
224
|
+
function digest(value) {
|
|
225
|
+
return (0, import_node_crypto.createHash)("sha256").update(value).digest();
|
|
226
|
+
}
|
|
227
|
+
function tokenEquals(a, b) {
|
|
228
|
+
return (0, import_node_crypto.timingSafeEqual)(digest(a), digest(b));
|
|
229
|
+
}
|
|
230
|
+
function readCookie(header, name) {
|
|
231
|
+
if (!header) return void 0;
|
|
232
|
+
for (const part of header.split(";")) {
|
|
233
|
+
const [k, ...rest] = part.trim().split("=");
|
|
234
|
+
if (k === name) {
|
|
235
|
+
try {
|
|
236
|
+
return decodeURIComponent(rest.join("="));
|
|
237
|
+
} catch {
|
|
238
|
+
return void 0;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return void 0;
|
|
243
|
+
}
|
|
244
|
+
function firstHeader(value) {
|
|
245
|
+
return Array.isArray(value) ? value[0] : value;
|
|
246
|
+
}
|
|
247
|
+
function deny(status, code, message) {
|
|
248
|
+
return { ok: false, status, code, message };
|
|
249
|
+
}
|
|
250
|
+
function createGuard(options) {
|
|
251
|
+
const strict = isLoopbackAddress(options.host);
|
|
252
|
+
const allowedHosts = options.allowedHosts.map((h) => h.toLowerCase());
|
|
253
|
+
const allowedOrigins = options.allowedOrigins.map((o) => o.toLowerCase().replace(/\/+$/, ""));
|
|
254
|
+
return (req) => {
|
|
255
|
+
const remote = req.socket.remoteAddress;
|
|
256
|
+
const remoteLoopback = isLoopbackAddress(remote);
|
|
257
|
+
const remoteAllowed = remoteLoopback || matchesAddress(remote, options.allowedRemoteAddresses);
|
|
258
|
+
if (strict && !remoteAllowed) {
|
|
259
|
+
return deny(
|
|
260
|
+
403,
|
|
261
|
+
"forbidden",
|
|
262
|
+
"nodeui is loopback-only; requests from other hosts are rejected"
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
if (!strict && options.allowedRemoteAddresses.length > 0 && !remoteAllowed) {
|
|
266
|
+
return deny(403, "forbidden", "remote address is not in the nodeui allow-list");
|
|
267
|
+
}
|
|
268
|
+
if (strict && !options.trustProxy && !matchesAddress(remote, options.allowedRemoteAddresses)) {
|
|
269
|
+
if (FORWARD_HEADERS.some((h) => req.headers[h] !== void 0)) {
|
|
270
|
+
return deny(
|
|
271
|
+
403,
|
|
272
|
+
"forbidden",
|
|
273
|
+
"proxied requests are rejected (set trustProxy to allow forwarded traffic)"
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const hostHeader = firstHeader(req.headers.host);
|
|
278
|
+
const hostname = hostnameFromHostHeader(hostHeader);
|
|
279
|
+
if (hostname !== null) {
|
|
280
|
+
const hostOk = isLoopbackHostname(hostname) || allowedHosts.includes(hostname) || !strict && allowedHosts.length === 0;
|
|
281
|
+
if (!hostOk) {
|
|
282
|
+
return deny(
|
|
283
|
+
403,
|
|
284
|
+
"forbidden",
|
|
285
|
+
`Host "${hostname}" is not allowed (add it to allowedHosts to use nodeui via this name)`
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const origin = firstHeader(req.headers.origin);
|
|
290
|
+
if (origin !== void 0) {
|
|
291
|
+
let originOk = false;
|
|
292
|
+
const normalized = origin.toLowerCase().replace(/\/+$/, "");
|
|
293
|
+
if (allowedOrigins.includes(normalized)) {
|
|
294
|
+
originOk = true;
|
|
295
|
+
} else {
|
|
296
|
+
try {
|
|
297
|
+
originOk = hostHeader !== void 0 && new URL(origin).host === hostHeader.toLowerCase();
|
|
298
|
+
} catch {
|
|
299
|
+
originOk = false;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (!originOk) {
|
|
303
|
+
return deny(403, "forbidden", "cross-origin requests to nodeui are rejected");
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (options.authToken) {
|
|
307
|
+
const authHeader = firstHeader(req.headers.authorization);
|
|
308
|
+
const bearer = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : void 0;
|
|
309
|
+
const queryToken = new URL(req.url ?? "/", "http://localhost").searchParams.get("token");
|
|
310
|
+
const cookieToken = readCookie(firstHeader(req.headers.cookie), AUTH_COOKIE);
|
|
311
|
+
const candidates = [
|
|
312
|
+
bearer,
|
|
313
|
+
firstHeader(req.headers["x-nodeui-token"]),
|
|
314
|
+
cookieToken,
|
|
315
|
+
queryToken ?? void 0
|
|
316
|
+
];
|
|
317
|
+
const matched = candidates.find((c) => c !== void 0 && tokenEquals(c, options.authToken));
|
|
318
|
+
if (matched === void 0) {
|
|
319
|
+
return deny(401, "unauthorized", "a valid nodeui token is required");
|
|
320
|
+
}
|
|
321
|
+
if (queryToken !== null && matched === queryToken) {
|
|
322
|
+
return { ok: true, setToken: queryToken };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return { ok: true };
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/confirmations.ts
|
|
330
|
+
var import_node_crypto2 = require("crypto");
|
|
147
331
|
var ConfirmationStore = class {
|
|
148
|
-
constructor(ttlMs) {
|
|
332
|
+
constructor(ttlMs, maxOutstanding = 100) {
|
|
149
333
|
this.ttlMs = ttlMs;
|
|
334
|
+
this.maxOutstanding = maxOutstanding;
|
|
150
335
|
}
|
|
151
336
|
ttlMs;
|
|
337
|
+
maxOutstanding;
|
|
152
338
|
nonces = /* @__PURE__ */ new Map();
|
|
153
339
|
issue() {
|
|
154
340
|
this.prune();
|
|
155
|
-
|
|
341
|
+
while (this.nonces.size >= this.maxOutstanding) {
|
|
342
|
+
const oldest = this.nonces.keys().next().value;
|
|
343
|
+
if (oldest === void 0) break;
|
|
344
|
+
this.nonces.delete(oldest);
|
|
345
|
+
}
|
|
346
|
+
const nonce = (0, import_node_crypto2.randomBytes)(16).toString("hex");
|
|
156
347
|
const expiresAtMs = Date.now() + this.ttlMs;
|
|
157
348
|
this.nonces.set(nonce, expiresAtMs);
|
|
158
349
|
return { nonce, expiresAtMs, ttlMs: this.ttlMs };
|
|
@@ -464,15 +655,65 @@ var EventLoopLagProvider = class {
|
|
|
464
655
|
// src/providers/health.ts
|
|
465
656
|
var LAG_OK_MS = 50;
|
|
466
657
|
var LAG_DEGRADED_MS = 200;
|
|
658
|
+
var CHECK_TIMEOUT_MS = 3e3;
|
|
659
|
+
var CHECK_CACHE_MS = 5e3;
|
|
660
|
+
function withTimeout(promise, ms) {
|
|
661
|
+
return new Promise((resolve3, reject) => {
|
|
662
|
+
const timer = setTimeout(() => reject(new Error(`timed out after ${ms}ms`)), ms);
|
|
663
|
+
promise.then(
|
|
664
|
+
(value) => {
|
|
665
|
+
clearTimeout(timer);
|
|
666
|
+
resolve3(value);
|
|
667
|
+
},
|
|
668
|
+
(err) => {
|
|
669
|
+
clearTimeout(timer);
|
|
670
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
671
|
+
}
|
|
672
|
+
);
|
|
673
|
+
});
|
|
674
|
+
}
|
|
467
675
|
var HealthProvider = class {
|
|
676
|
+
constructor(checks = {}) {
|
|
677
|
+
this.checks = checks;
|
|
678
|
+
}
|
|
679
|
+
checks;
|
|
468
680
|
id = "health";
|
|
469
|
-
|
|
681
|
+
cache = null;
|
|
682
|
+
async runChecks() {
|
|
683
|
+
const names = Object.keys(this.checks);
|
|
684
|
+
if (names.length === 0) return [];
|
|
685
|
+
if (this.cache && Date.now() - this.cache.at < CHECK_CACHE_MS) return this.cache.results;
|
|
686
|
+
const results = await Promise.all(
|
|
687
|
+
names.map(async (name) => {
|
|
688
|
+
const started = Date.now();
|
|
689
|
+
try {
|
|
690
|
+
const outcome = await withTimeout(
|
|
691
|
+
Promise.resolve().then(() => this.checks[name]()),
|
|
692
|
+
CHECK_TIMEOUT_MS
|
|
693
|
+
);
|
|
694
|
+
if (outcome === false) throw new Error("check returned false");
|
|
695
|
+
return { name, status: "up", durationMs: Date.now() - started };
|
|
696
|
+
} catch (err) {
|
|
697
|
+
return {
|
|
698
|
+
name,
|
|
699
|
+
status: "down",
|
|
700
|
+
durationMs: Date.now() - started,
|
|
701
|
+
error: err instanceof Error ? err.message : String(err)
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
})
|
|
705
|
+
);
|
|
706
|
+
this.cache = { at: Date.now(), results };
|
|
707
|
+
return results;
|
|
708
|
+
}
|
|
709
|
+
async get(ctx) {
|
|
470
710
|
const loop = ctx.store["event-loop"];
|
|
471
711
|
const memory = ctx.store.memory;
|
|
472
712
|
const lagMs = typeof loop?.currentMs === "number" ? loop.currentMs : null;
|
|
473
713
|
const totalMem = typeof memory?.totalMem === "number" ? memory.totalMem : 0;
|
|
474
714
|
const rss = typeof memory?.rss === "number" ? memory.rss : 0;
|
|
475
715
|
const memoryUsedPercent = totalMem > 0 ? rss / totalMem * 100 : null;
|
|
716
|
+
const checks = await this.runChecks();
|
|
476
717
|
let status;
|
|
477
718
|
let statusReason;
|
|
478
719
|
if (lagMs === null) {
|
|
@@ -488,6 +729,11 @@ var HealthProvider = class {
|
|
|
488
729
|
status = "critical";
|
|
489
730
|
statusReason = `event-loop lag ${lagMs.toFixed(1)}ms above ${LAG_DEGRADED_MS}ms`;
|
|
490
731
|
}
|
|
732
|
+
const down = checks.filter((c) => c.status === "down");
|
|
733
|
+
if (down.length > 0) {
|
|
734
|
+
status = "critical";
|
|
735
|
+
statusReason = `dependency down: ${down.map((c) => c.name).join(", ")}`;
|
|
736
|
+
}
|
|
491
737
|
return {
|
|
492
738
|
ok: true,
|
|
493
739
|
data: {
|
|
@@ -498,19 +744,202 @@ var HealthProvider = class {
|
|
|
498
744
|
nodeVersion: process.version,
|
|
499
745
|
platform: process.platform,
|
|
500
746
|
eventLoopLagMs: lagMs,
|
|
501
|
-
memoryUsedPercent
|
|
747
|
+
memoryUsedPercent,
|
|
748
|
+
checks
|
|
502
749
|
}
|
|
503
750
|
};
|
|
504
751
|
}
|
|
505
752
|
};
|
|
506
753
|
|
|
754
|
+
// src/providers/outgoing.ts
|
|
755
|
+
var import_node_diagnostics_channel = __toESM(require("diagnostics_channel"));
|
|
756
|
+
var import_node_http = __toESM(require("http"));
|
|
757
|
+
var import_node_https = __toESM(require("https"));
|
|
758
|
+
var import_node_module = require("module");
|
|
759
|
+
var RESPONSE_PAGE_SIZE = 100;
|
|
760
|
+
var recorders = /* @__PURE__ */ new Set();
|
|
761
|
+
var originals = /* @__PURE__ */ new Map();
|
|
762
|
+
var undiciUnsubscribe = null;
|
|
763
|
+
function describeTarget(defaultProtocol, args) {
|
|
764
|
+
try {
|
|
765
|
+
let method = "GET";
|
|
766
|
+
let host = "unknown";
|
|
767
|
+
let path = "/";
|
|
768
|
+
const [first, second] = args;
|
|
769
|
+
const opts = typeof first === "string" || first instanceof URL ? second : first;
|
|
770
|
+
if (typeof first === "string" || first instanceof URL) {
|
|
771
|
+
const parsed = new URL(first.toString());
|
|
772
|
+
host = parsed.host;
|
|
773
|
+
path = parsed.pathname;
|
|
774
|
+
}
|
|
775
|
+
if (opts && typeof opts === "object") {
|
|
776
|
+
if (typeof opts.method === "string") method = opts.method.toUpperCase();
|
|
777
|
+
if (typeof first !== "string" && !(first instanceof URL)) {
|
|
778
|
+
const name = opts.hostname ?? opts.host ?? "localhost";
|
|
779
|
+
host = opts.port ? `${name}:${String(opts.port)}` : name;
|
|
780
|
+
path = typeof opts.path === "string" ? opts.path.split("?")[0] ?? "/" : "/";
|
|
781
|
+
const protocol = typeof opts.protocol === "string" ? opts.protocol : defaultProtocol;
|
|
782
|
+
return { method, url: `${protocol}//${host}${path}` };
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return { method, url: `${defaultProtocol}//${host}${path}` };
|
|
786
|
+
} catch {
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
function emit(entry) {
|
|
791
|
+
for (const record of recorders) {
|
|
792
|
+
try {
|
|
793
|
+
record(entry);
|
|
794
|
+
} catch {
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
function wrapModule(mod, protocol) {
|
|
799
|
+
for (const name of ["request", "get"]) {
|
|
800
|
+
const key = `${protocol}${name}`;
|
|
801
|
+
const original = mod[name];
|
|
802
|
+
originals.set(key, original);
|
|
803
|
+
const wrapped = function(...args) {
|
|
804
|
+
const req = original.apply(this, args);
|
|
805
|
+
const target = recorders.size > 0 ? describeTarget(protocol, args) : null;
|
|
806
|
+
if (target && req && typeof req.once === "function") {
|
|
807
|
+
const started = process.hrtime.bigint();
|
|
808
|
+
const timestampMs = Date.now();
|
|
809
|
+
let done = false;
|
|
810
|
+
const finish = (status, error) => {
|
|
811
|
+
if (done) return;
|
|
812
|
+
done = true;
|
|
813
|
+
emit({
|
|
814
|
+
...target,
|
|
815
|
+
status,
|
|
816
|
+
durationMs: Number(process.hrtime.bigint() - started) / 1e6,
|
|
817
|
+
timestampMs,
|
|
818
|
+
...error ? { error } : {}
|
|
819
|
+
});
|
|
820
|
+
};
|
|
821
|
+
req.once("response", (res) => {
|
|
822
|
+
const status = res.statusCode ?? null;
|
|
823
|
+
res.once("end", () => finish(status));
|
|
824
|
+
res.once("close", () => finish(status));
|
|
825
|
+
});
|
|
826
|
+
req.once("error", (err) => finish(null, err.message));
|
|
827
|
+
}
|
|
828
|
+
return req;
|
|
829
|
+
};
|
|
830
|
+
mod[name] = wrapped;
|
|
831
|
+
}
|
|
832
|
+
(0, import_node_module.syncBuiltinESMExports)();
|
|
833
|
+
}
|
|
834
|
+
function unwrapModule(mod, protocol) {
|
|
835
|
+
for (const name of ["request", "get"]) {
|
|
836
|
+
const original = originals.get(`${protocol}${name}`);
|
|
837
|
+
if (original) mod[name] = original;
|
|
838
|
+
originals.delete(`${protocol}${name}`);
|
|
839
|
+
}
|
|
840
|
+
(0, import_node_module.syncBuiltinESMExports)();
|
|
841
|
+
}
|
|
842
|
+
function subscribeUndici() {
|
|
843
|
+
const started = /* @__PURE__ */ new WeakMap();
|
|
844
|
+
const statuses = /* @__PURE__ */ new WeakMap();
|
|
845
|
+
const target = (req) => ({
|
|
846
|
+
method: (req.method ?? "GET").toUpperCase(),
|
|
847
|
+
url: `${String(req.origin ?? "")}${(req.path ?? "/").split("?")[0]}`
|
|
848
|
+
});
|
|
849
|
+
const onCreate = (message) => {
|
|
850
|
+
const request = message.request;
|
|
851
|
+
if (request) started.set(request, { at: process.hrtime.bigint(), ts: Date.now() });
|
|
852
|
+
};
|
|
853
|
+
const onHeaders = (message) => {
|
|
854
|
+
const { request, response } = message;
|
|
855
|
+
if (request && typeof response?.statusCode === "number") {
|
|
856
|
+
statuses.set(request, response.statusCode);
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
const finish = (request, error) => {
|
|
860
|
+
if (!request) return;
|
|
861
|
+
const begin = started.get(request);
|
|
862
|
+
if (!begin) return;
|
|
863
|
+
started.delete(request);
|
|
864
|
+
emit({
|
|
865
|
+
...target(request),
|
|
866
|
+
status: statuses.get(request) ?? null,
|
|
867
|
+
durationMs: Number(process.hrtime.bigint() - begin.at) / 1e6,
|
|
868
|
+
timestampMs: begin.ts,
|
|
869
|
+
...error ? { error } : {}
|
|
870
|
+
});
|
|
871
|
+
};
|
|
872
|
+
const onTrailers = (message) => finish(message.request);
|
|
873
|
+
const onError = (message) => {
|
|
874
|
+
const { request, error } = message;
|
|
875
|
+
finish(request, error?.message ?? "request failed");
|
|
876
|
+
};
|
|
877
|
+
import_node_diagnostics_channel.default.subscribe("undici:request:create", onCreate);
|
|
878
|
+
import_node_diagnostics_channel.default.subscribe("undici:request:headers", onHeaders);
|
|
879
|
+
import_node_diagnostics_channel.default.subscribe("undici:request:trailers", onTrailers);
|
|
880
|
+
import_node_diagnostics_channel.default.subscribe("undici:request:error", onError);
|
|
881
|
+
return () => {
|
|
882
|
+
import_node_diagnostics_channel.default.unsubscribe("undici:request:create", onCreate);
|
|
883
|
+
import_node_diagnostics_channel.default.unsubscribe("undici:request:headers", onHeaders);
|
|
884
|
+
import_node_diagnostics_channel.default.unsubscribe("undici:request:trailers", onTrailers);
|
|
885
|
+
import_node_diagnostics_channel.default.unsubscribe("undici:request:error", onError);
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
function interceptOutgoing(record) {
|
|
889
|
+
const first = recorders.size === 0;
|
|
890
|
+
recorders.add(record);
|
|
891
|
+
if (first) {
|
|
892
|
+
wrapModule(import_node_http.default, "http:");
|
|
893
|
+
wrapModule(import_node_https.default, "https:");
|
|
894
|
+
try {
|
|
895
|
+
undiciUnsubscribe = subscribeUndici();
|
|
896
|
+
} catch {
|
|
897
|
+
undiciUnsubscribe = null;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
return () => {
|
|
901
|
+
if (!recorders.delete(record) || recorders.size > 0) return;
|
|
902
|
+
unwrapModule(import_node_http.default, "http:");
|
|
903
|
+
unwrapModule(import_node_https.default, "https:");
|
|
904
|
+
undiciUnsubscribe?.();
|
|
905
|
+
undiciUnsubscribe = null;
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
var OutgoingProvider = class {
|
|
909
|
+
id = "outgoing";
|
|
910
|
+
buffer;
|
|
911
|
+
nextId = 1;
|
|
912
|
+
failed = 0;
|
|
913
|
+
release = null;
|
|
914
|
+
constructor(size) {
|
|
915
|
+
this.buffer = new RingBuffer(size);
|
|
916
|
+
}
|
|
917
|
+
start() {
|
|
918
|
+
if (this.release) return;
|
|
919
|
+
this.release = interceptOutgoing((entry) => this.record(entry));
|
|
920
|
+
}
|
|
921
|
+
stop() {
|
|
922
|
+
this.release?.();
|
|
923
|
+
this.release = null;
|
|
924
|
+
}
|
|
925
|
+
record(entry) {
|
|
926
|
+
this.buffer.push({ id: this.nextId, ...entry });
|
|
927
|
+
this.nextId += 1;
|
|
928
|
+
if (entry.error || entry.status !== null && entry.status >= 500) this.failed += 1;
|
|
929
|
+
}
|
|
930
|
+
get() {
|
|
931
|
+
const entries = this.buffer.slice(Math.max(0, this.buffer.length - RESPONSE_PAGE_SIZE));
|
|
932
|
+
return { ok: true, data: { total: this.nextId - 1, failed: this.failed, entries } };
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
|
|
507
936
|
// src/providers/heap-snapshot.ts
|
|
508
937
|
var import_promises = require("fs/promises");
|
|
509
938
|
var import_node_os2 = require("os");
|
|
510
939
|
var import_node_path2 = require("path");
|
|
511
940
|
var import_node_v8 = require("v8");
|
|
512
941
|
function defaultSnapshotDir() {
|
|
513
|
-
return process.env.NODEUI_HEAP_SNAPSHOT_DIR ?? (0, import_node_os2.tmpdir)();
|
|
942
|
+
return process.env.NODEUI_HEAP_SNAPSHOT_DIR ?? (0, import_node_path2.join)((0, import_node_os2.tmpdir)(), "nodeui-heap");
|
|
514
943
|
}
|
|
515
944
|
var HeapSnapshotProvider = class {
|
|
516
945
|
id = "heap-snapshot";
|
|
@@ -524,8 +953,9 @@ var HeapSnapshotProvider = class {
|
|
|
524
953
|
const fileName = `nodeui-heap-${process.pid}-${stamp}.heapsnapshot`;
|
|
525
954
|
const filePath = (0, import_node_path2.join)(dir, fileName);
|
|
526
955
|
try {
|
|
527
|
-
await (0, import_promises.mkdir)(dir, { recursive: true });
|
|
956
|
+
await (0, import_promises.mkdir)(dir, { recursive: true, mode: 448 });
|
|
528
957
|
(0, import_node_v8.writeHeapSnapshot)(filePath);
|
|
958
|
+
await (0, import_promises.chmod)(filePath, 384).catch(() => void 0);
|
|
529
959
|
const fileStat = await (0, import_promises.stat)(filePath);
|
|
530
960
|
const data = {
|
|
531
961
|
fileName,
|
|
@@ -548,7 +978,55 @@ var HeapSnapshotProvider = class {
|
|
|
548
978
|
};
|
|
549
979
|
|
|
550
980
|
// src/providers/requests.ts
|
|
551
|
-
var
|
|
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
|
+
}
|
|
552
1030
|
var RequestsProvider = class {
|
|
553
1031
|
id = "requests";
|
|
554
1032
|
buffer;
|
|
@@ -564,8 +1042,9 @@ var RequestsProvider = class {
|
|
|
564
1042
|
return this.buffer.length;
|
|
565
1043
|
}
|
|
566
1044
|
get() {
|
|
567
|
-
const entries = this.buffer.slice(Math.max(0, this.buffer.length -
|
|
568
|
-
|
|
1045
|
+
const entries = this.buffer.slice(Math.max(0, this.buffer.length - RESPONSE_PAGE_SIZE2));
|
|
1046
|
+
const summary = summarizeRequests(this.buffer.toArray());
|
|
1047
|
+
return { ok: true, data: { total: this.buffer.length, entries, summary } };
|
|
569
1048
|
}
|
|
570
1049
|
};
|
|
571
1050
|
|
|
@@ -679,6 +1158,11 @@ var RoutesProvider = class {
|
|
|
679
1158
|
id = "routes";
|
|
680
1159
|
cache = /* @__PURE__ */ new WeakMap();
|
|
681
1160
|
get(ctx) {
|
|
1161
|
+
const source = ctx.store["routes-source"];
|
|
1162
|
+
if (source !== void 0) {
|
|
1163
|
+
const routes2 = typeof source === "function" ? source() : source;
|
|
1164
|
+
return { ok: true, data: { routes: routes2 } };
|
|
1165
|
+
}
|
|
682
1166
|
const router = ctx.store["express-router"];
|
|
683
1167
|
if (!router) {
|
|
684
1168
|
return {
|
|
@@ -727,14 +1211,14 @@ function formatArg(arg) {
|
|
|
727
1211
|
var refs = 0;
|
|
728
1212
|
var pushes = /* @__PURE__ */ new Set();
|
|
729
1213
|
var wrappers = /* @__PURE__ */ new Map();
|
|
730
|
-
var
|
|
1214
|
+
var originals2 = {};
|
|
731
1215
|
function consoleRef() {
|
|
732
1216
|
return console;
|
|
733
1217
|
}
|
|
734
1218
|
function installWrappers() {
|
|
735
1219
|
for (const [method, level] of CONSOLE_METHODS) {
|
|
736
1220
|
const original = consoleRef()[method];
|
|
737
|
-
|
|
1221
|
+
originals2[method] = original;
|
|
738
1222
|
const wrapper = (...args) => {
|
|
739
1223
|
for (const push of pushes) {
|
|
740
1224
|
try {
|
|
@@ -752,7 +1236,7 @@ function uninstallWrappers() {
|
|
|
752
1236
|
for (const [method] of CONSOLE_METHODS) {
|
|
753
1237
|
const current = consoleRef()[method];
|
|
754
1238
|
if (current === wrappers.get(method)) {
|
|
755
|
-
const original =
|
|
1239
|
+
const original = originals2[method];
|
|
756
1240
|
if (original) {
|
|
757
1241
|
consoleRef()[method] = original;
|
|
758
1242
|
} else {
|
|
@@ -761,7 +1245,7 @@ function uninstallWrappers() {
|
|
|
761
1245
|
}
|
|
762
1246
|
}
|
|
763
1247
|
wrappers.clear();
|
|
764
|
-
|
|
1248
|
+
originals2 = {};
|
|
765
1249
|
}
|
|
766
1250
|
function interceptConsole(push) {
|
|
767
1251
|
refs += 1;
|
|
@@ -808,9 +1292,10 @@ var LogsProvider = class {
|
|
|
808
1292
|
};
|
|
809
1293
|
|
|
810
1294
|
// src/sse.ts
|
|
811
|
-
function startSse(res) {
|
|
1295
|
+
function startSse(res, extraHeaders = {}) {
|
|
812
1296
|
let closed = false;
|
|
813
1297
|
res.writeHead(200, {
|
|
1298
|
+
...extraHeaders,
|
|
814
1299
|
"Content-Type": "text/event-stream",
|
|
815
1300
|
"Cache-Control": "no-cache",
|
|
816
1301
|
Connection: "keep-alive",
|
|
@@ -841,8 +1326,35 @@ var import_node_os3 = require("os");
|
|
|
841
1326
|
var import_node_path3 = require("path");
|
|
842
1327
|
var STATIC_ROOT = (0, import_node_path3.resolve)(__dirname, "..", "static");
|
|
843
1328
|
var SSE_HEARTBEAT_MS = 15e3;
|
|
844
|
-
function serializeEnvelope(envelope) {
|
|
845
|
-
return JSON.stringify(maskSecrets(envelope));
|
|
1329
|
+
function serializeEnvelope(envelope, mask = true) {
|
|
1330
|
+
return JSON.stringify(mask ? maskSecrets(envelope) : envelope);
|
|
1331
|
+
}
|
|
1332
|
+
var BUILT_IN_PANELS = /* @__PURE__ */ new Set([
|
|
1333
|
+
"health",
|
|
1334
|
+
"memory",
|
|
1335
|
+
"cpu",
|
|
1336
|
+
"event-loop",
|
|
1337
|
+
"heap-snapshot",
|
|
1338
|
+
"startup",
|
|
1339
|
+
"requests",
|
|
1340
|
+
"env",
|
|
1341
|
+
"routes",
|
|
1342
|
+
"logs",
|
|
1343
|
+
"metrics",
|
|
1344
|
+
"outgoing"
|
|
1345
|
+
]);
|
|
1346
|
+
var PLUGIN_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
1347
|
+
var RESERVED_API_NAMES = /* @__PURE__ */ new Set(["config", "live", "confirmations"]);
|
|
1348
|
+
var SECURITY_HEADERS = {
|
|
1349
|
+
"X-Content-Type-Options": "nosniff",
|
|
1350
|
+
"X-Frame-Options": "DENY",
|
|
1351
|
+
"Referrer-Policy": "no-referrer",
|
|
1352
|
+
"Cross-Origin-Resource-Policy": "same-origin"
|
|
1353
|
+
};
|
|
1354
|
+
var CSP = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'";
|
|
1355
|
+
function listFrom(value, raw) {
|
|
1356
|
+
if (value) return value;
|
|
1357
|
+
return (raw ?? "").split(",").map((v) => v.trim()).filter(Boolean);
|
|
846
1358
|
}
|
|
847
1359
|
function pathnameOf(req) {
|
|
848
1360
|
const raw = req.originalUrl ?? req.url ?? "/";
|
|
@@ -902,19 +1414,46 @@ function createNodeUI(options = {}) {
|
|
|
902
1414
|
env.NODEUI_CONFIRM_TTL_MS ? Number(env.NODEUI_CONFIRM_TTL_MS) : 6e4,
|
|
903
1415
|
"NODEUI_CONFIRM_TTL_MS"
|
|
904
1416
|
),
|
|
905
|
-
heapSnapshotDir: options.heapSnapshotDir ?? env.NODEUI_HEAP_SNAPSHOT_DIR ?? (0, import_node_os3.tmpdir)()
|
|
1417
|
+
heapSnapshotDir: options.heapSnapshotDir ?? env.NODEUI_HEAP_SNAPSHOT_DIR ?? (0, import_node_path3.join)((0, import_node_os3.tmpdir)(), "nodeui-heap"),
|
|
1418
|
+
allowedHosts: listFrom(options.allowedHosts, env.NODEUI_ALLOWED_HOSTS),
|
|
1419
|
+
allowedOrigins: listFrom(options.allowedOrigins, env.NODEUI_ALLOWED_ORIGINS),
|
|
1420
|
+
allowedRemoteAddresses: listFrom(options.allowedRemoteAddresses, env.NODEUI_ALLOWED_REMOTE),
|
|
1421
|
+
trustProxy: options.trustProxy ?? env.NODEUI_TRUST_PROXY === "true",
|
|
1422
|
+
authRequired: Boolean(options.authToken ?? env.NODEUI_TOKEN),
|
|
1423
|
+
maxSseClients: positiveInt(
|
|
1424
|
+
options.maxSseClients,
|
|
1425
|
+
env.NODEUI_MAX_SSE_CLIENTS ? Number(env.NODEUI_MAX_SSE_CLIENTS) : 10,
|
|
1426
|
+
"NODEUI_MAX_SSE_CLIENTS"
|
|
1427
|
+
)
|
|
906
1428
|
};
|
|
907
|
-
|
|
1429
|
+
const authToken = options.authToken ?? env.NODEUI_TOKEN;
|
|
1430
|
+
const guard = createGuard({
|
|
1431
|
+
host: config.host,
|
|
1432
|
+
allowedHosts: config.allowedHosts,
|
|
1433
|
+
allowedOrigins: config.allowedOrigins,
|
|
1434
|
+
allowedRemoteAddresses: config.allowedRemoteAddresses,
|
|
1435
|
+
trustProxy: config.trustProxy,
|
|
1436
|
+
authToken: authToken || void 0
|
|
1437
|
+
});
|
|
1438
|
+
if (config.enabled && !isLoopbackAddress(config.host)) {
|
|
908
1439
|
console.warn(
|
|
909
|
-
`[nodeui] NODEUI_HOST is set to non-loopback "${config.host}". The developer console will be reachable from other hosts; only do this deliberately
|
|
1440
|
+
`[nodeui] NODEUI_HOST is set to non-loopback "${config.host}". The developer console will be reachable from other hosts; only do this deliberately` + (config.authRequired ? "." : " and set NODEUI_TOKEN (authToken) to require a token.")
|
|
910
1441
|
);
|
|
911
1442
|
}
|
|
1443
|
+
if (config.enabled && env.NODE_ENV === "production") {
|
|
1444
|
+
console.warn(
|
|
1445
|
+
"[nodeui] running with NODE_ENV=production. The console exposes environment, logs and heap data; keep it behind loopback and an auth token."
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
if (config.enabled && !config.maskSecrets) {
|
|
1449
|
+
console.warn("[nodeui] secret masking is disabled; panels may show credentials.");
|
|
1450
|
+
}
|
|
912
1451
|
const ctx = { config, env, store: {} };
|
|
913
1452
|
if (options.config !== void 0) {
|
|
914
1453
|
ctx.store["app-config"] = options.config;
|
|
915
1454
|
}
|
|
916
1455
|
const registry = new ProviderRegistry();
|
|
917
|
-
registry.register(new HealthProvider());
|
|
1456
|
+
registry.register(new HealthProvider(options.healthChecks));
|
|
918
1457
|
registry.register(new MemoryProvider());
|
|
919
1458
|
registry.register(new CpuProvider());
|
|
920
1459
|
registry.register(new EventLoopLagProvider());
|
|
@@ -936,9 +1475,23 @@ function createNodeUI(options = {}) {
|
|
|
936
1475
|
registry.register(envProvider);
|
|
937
1476
|
const routesProvider = new RoutesProvider();
|
|
938
1477
|
registry.register(routesProvider);
|
|
1478
|
+
registry.register(
|
|
1479
|
+
new OutgoingProvider(positiveInt(options.outgoingLogSize, 200, "outgoingLogSize"))
|
|
1480
|
+
);
|
|
1481
|
+
const pluginMeta = [];
|
|
1482
|
+
for (const plugin of options.plugins ?? []) {
|
|
1483
|
+
if (!PLUGIN_ID_PATTERN.test(plugin.id) || BUILT_IN_PANELS.has(plugin.id) || RESERVED_API_NAMES.has(plugin.id) || registry.get(plugin.id)) {
|
|
1484
|
+
throw new Error(
|
|
1485
|
+
`nodeui plugin id "${plugin.id}" is invalid or already taken (use lowercase letters, digits and "-")`
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1488
|
+
registry.register(plugin);
|
|
1489
|
+
pluginMeta.push({ id: plugin.id, title: plugin.title ?? plugin.id });
|
|
1490
|
+
}
|
|
939
1491
|
const confirmations = new ConfirmationStore(config.confirmTtlMs);
|
|
940
1492
|
const activeProviders = /* @__PURE__ */ new Set();
|
|
941
1493
|
let inactivityTimer = null;
|
|
1494
|
+
let sseClients = 0;
|
|
942
1495
|
function stopAll() {
|
|
943
1496
|
for (const id of activeProviders) {
|
|
944
1497
|
registry.get(id)?.stop?.(ctx);
|
|
@@ -962,8 +1515,9 @@ function createNodeUI(options = {}) {
|
|
|
962
1515
|
resetInactivityTimer();
|
|
963
1516
|
}
|
|
964
1517
|
function sendJson(res, status, envelope) {
|
|
965
|
-
const body = serializeEnvelope(envelope);
|
|
1518
|
+
const body = serializeEnvelope(envelope, config.maskSecrets);
|
|
966
1519
|
res.writeHead(status, {
|
|
1520
|
+
...SECURITY_HEADERS,
|
|
967
1521
|
"Content-Type": "application/json",
|
|
968
1522
|
"Content-Length": Buffer.byteLength(body),
|
|
969
1523
|
"Cache-Control": "no-store"
|
|
@@ -981,8 +1535,24 @@ function createNodeUI(options = {}) {
|
|
|
981
1535
|
const provider = registry.get(id);
|
|
982
1536
|
if (!provider) return notFound(res);
|
|
983
1537
|
const requestCtx = query ? { ...ctx, query } : ctx;
|
|
984
|
-
|
|
985
|
-
|
|
1538
|
+
sendJson(res, ...await readPanel(provider, requestCtx));
|
|
1539
|
+
}
|
|
1540
|
+
async function readPanel(provider, requestCtx) {
|
|
1541
|
+
try {
|
|
1542
|
+
const result = await provider.get(requestCtx);
|
|
1543
|
+
return [result.ok ? 200 : 500, result];
|
|
1544
|
+
} catch (err) {
|
|
1545
|
+
return [
|
|
1546
|
+
500,
|
|
1547
|
+
{
|
|
1548
|
+
ok: false,
|
|
1549
|
+
error: {
|
|
1550
|
+
code: "provider-failed",
|
|
1551
|
+
message: err instanceof Error ? err.message : "provider failed"
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
];
|
|
1555
|
+
}
|
|
986
1556
|
}
|
|
987
1557
|
function configData() {
|
|
988
1558
|
return {
|
|
@@ -995,7 +1565,9 @@ function createNodeUI(options = {}) {
|
|
|
995
1565
|
logSize: config.logSize,
|
|
996
1566
|
pollIntervalMs: config.pollIntervalMs,
|
|
997
1567
|
panels: registry.ids(),
|
|
998
|
-
masking: { enabled: config.maskSecrets, pattern: SECRET_KEY_PATTERN.source }
|
|
1568
|
+
masking: { enabled: config.maskSecrets, pattern: SECRET_KEY_PATTERN.source },
|
|
1569
|
+
locked: config.authRequired,
|
|
1570
|
+
plugins: pluginMeta
|
|
999
1571
|
};
|
|
1000
1572
|
}
|
|
1001
1573
|
function issueConfirmation(res) {
|
|
@@ -1027,22 +1599,37 @@ function createNodeUI(options = {}) {
|
|
|
1027
1599
|
const requested = (search.get("panels") ?? "").split(",").filter(Boolean);
|
|
1028
1600
|
const panels = (requested.length > 0 ? requested.filter((id) => registry.get(id) !== void 0) : registry.ids()).filter((value, index, self) => self.indexOf(value) === index);
|
|
1029
1601
|
if (panels.length === 0) return notFound(res);
|
|
1030
|
-
|
|
1602
|
+
if (sseClients >= config.maxSseClients) {
|
|
1603
|
+
res.writeHead(429, { ...SECURITY_HEADERS, "Content-Type": "application/json" });
|
|
1604
|
+
res.end(
|
|
1605
|
+
JSON.stringify({
|
|
1606
|
+
ok: false,
|
|
1607
|
+
error: { code: "too-many-streams", message: "too many open live streams" }
|
|
1608
|
+
})
|
|
1609
|
+
);
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
sseClients += 1;
|
|
1613
|
+
const stream = startSse(res, SECURITY_HEADERS);
|
|
1031
1614
|
const push = async () => {
|
|
1032
1615
|
for (const id of panels) {
|
|
1033
1616
|
ensureActive(id);
|
|
1034
1617
|
const provider = registry.get(id);
|
|
1035
1618
|
if (!provider) continue;
|
|
1036
|
-
const result = await provider
|
|
1037
|
-
stream.send({ panel: id, envelope: result });
|
|
1619
|
+
const [, result] = await readPanel(provider, ctx);
|
|
1620
|
+
stream.send({ panel: id, envelope: config.maskSecrets ? maskSecrets(result) : result });
|
|
1038
1621
|
}
|
|
1039
1622
|
};
|
|
1040
|
-
void push();
|
|
1041
|
-
const pushTimer = setInterval(() => void push(), config.pollIntervalMs);
|
|
1623
|
+
void push().catch(() => void 0);
|
|
1624
|
+
const pushTimer = setInterval(() => void push().catch(() => void 0), config.pollIntervalMs);
|
|
1042
1625
|
const heartbeatTimer = setInterval(() => stream.heartbeat(), SSE_HEARTBEAT_MS);
|
|
1043
1626
|
if (typeof pushTimer.unref === "function") pushTimer.unref();
|
|
1044
1627
|
if (typeof heartbeatTimer.unref === "function") heartbeatTimer.unref();
|
|
1628
|
+
let cleaned = false;
|
|
1045
1629
|
const cleanup = () => {
|
|
1630
|
+
if (cleaned) return;
|
|
1631
|
+
cleaned = true;
|
|
1632
|
+
sseClients -= 1;
|
|
1046
1633
|
clearInterval(pushTimer);
|
|
1047
1634
|
clearInterval(heartbeatTimer);
|
|
1048
1635
|
stream.close();
|
|
@@ -1085,30 +1672,33 @@ function createNodeUI(options = {}) {
|
|
|
1085
1672
|
return;
|
|
1086
1673
|
}
|
|
1087
1674
|
res.writeHead(200, {
|
|
1675
|
+
...SECURITY_HEADERS,
|
|
1676
|
+
"Content-Security-Policy": CSP,
|
|
1088
1677
|
"Content-Type": asset.contentType,
|
|
1089
1678
|
"Content-Length": asset.length,
|
|
1090
1679
|
"Cache-Control": "no-cache"
|
|
1091
1680
|
});
|
|
1092
1681
|
asset.content.pipe(res);
|
|
1093
1682
|
}
|
|
1094
|
-
function guardLoopback(req, res) {
|
|
1095
|
-
if (isLoopbackAddress(config.host)) {
|
|
1096
|
-
const remote = req.socket.remoteAddress;
|
|
1097
|
-
if (!isLoopbackAddress(remote)) {
|
|
1098
|
-
sendJson(res, 403, {
|
|
1099
|
-
ok: false,
|
|
1100
|
-
error: {
|
|
1101
|
-
code: "forbidden",
|
|
1102
|
-
message: "nodeui is loopback-only; requests from other hosts are rejected"
|
|
1103
|
-
}
|
|
1104
|
-
});
|
|
1105
|
-
return false;
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
return true;
|
|
1109
|
-
}
|
|
1110
1683
|
async function handleNodeUIPath(req, res, urlPath) {
|
|
1111
|
-
|
|
1684
|
+
const verdict = guard(req);
|
|
1685
|
+
if (!verdict.ok) {
|
|
1686
|
+
sendJson(res, verdict.status, {
|
|
1687
|
+
ok: false,
|
|
1688
|
+
error: { code: verdict.code, message: verdict.message }
|
|
1689
|
+
});
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
if (verdict.setToken !== void 0) {
|
|
1693
|
+
res.writeHead(302, {
|
|
1694
|
+
...SECURITY_HEADERS,
|
|
1695
|
+
"Set-Cookie": `${AUTH_COOKIE}=${encodeURIComponent(verdict.setToken)}; Path=${path}; HttpOnly; SameSite=Strict`,
|
|
1696
|
+
Location: `${path}/`,
|
|
1697
|
+
"Cache-Control": "no-store"
|
|
1698
|
+
});
|
|
1699
|
+
res.end();
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1112
1702
|
const apiBase = `${path}/api`;
|
|
1113
1703
|
if (urlPath === apiBase || urlPath.startsWith(`${apiBase}/`)) {
|
|
1114
1704
|
await handleApi(req, res, urlPath.slice(apiBase.length));
|
|
@@ -1192,6 +1782,9 @@ function createNodeUI(options = {}) {
|
|
|
1192
1782
|
}
|
|
1193
1783
|
notFound(res);
|
|
1194
1784
|
},
|
|
1785
|
+
setRoutes(source) {
|
|
1786
|
+
ctx.store["routes-source"] = source;
|
|
1787
|
+
},
|
|
1195
1788
|
mark(name) {
|
|
1196
1789
|
startupTracker.mark(name);
|
|
1197
1790
|
},
|
|
@@ -1209,6 +1802,7 @@ function createNodeUI(options = {}) {
|
|
|
1209
1802
|
}
|
|
1210
1803
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1211
1804
|
0 && (module.exports = {
|
|
1805
|
+
AUTH_COOKIE,
|
|
1212
1806
|
ConfirmationStore,
|
|
1213
1807
|
CpuProvider,
|
|
1214
1808
|
DEFAULT_LOG_SIZE,
|
|
@@ -1219,6 +1813,7 @@ function createNodeUI(options = {}) {
|
|
|
1219
1813
|
LogsProvider,
|
|
1220
1814
|
MemoryProvider,
|
|
1221
1815
|
MetricsProvider,
|
|
1816
|
+
OutgoingProvider,
|
|
1222
1817
|
ProviderRegistry,
|
|
1223
1818
|
RequestsProvider,
|
|
1224
1819
|
RingBuffer,
|
|
@@ -1227,14 +1822,21 @@ function createNodeUI(options = {}) {
|
|
|
1227
1822
|
SECRET_MASKED,
|
|
1228
1823
|
Sampler,
|
|
1229
1824
|
StartupTracker,
|
|
1825
|
+
createGuard,
|
|
1230
1826
|
createNodeUI,
|
|
1231
1827
|
extractRoutes,
|
|
1828
|
+
hostnameFromHostHeader,
|
|
1232
1829
|
interceptConsole,
|
|
1830
|
+
interceptOutgoing,
|
|
1233
1831
|
isLoopbackAddress,
|
|
1832
|
+
isLoopbackHostname,
|
|
1833
|
+
maskSecretText,
|
|
1234
1834
|
maskSecrets,
|
|
1835
|
+
matchesAddress,
|
|
1235
1836
|
resolveActivation,
|
|
1236
1837
|
resolveStaticAsset,
|
|
1237
1838
|
serializeEnvelope,
|
|
1238
|
-
startSse
|
|
1839
|
+
startSse,
|
|
1840
|
+
summarizeRequests
|
|
1239
1841
|
});
|
|
1240
1842
|
//# sourceMappingURL=index.cjs.map
|