@singhak/nodeui-core 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -82
- package/dist/index.cjs +598 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +168 -10
- package/dist/index.d.ts +168 -10
- package/dist/index.mjs +580 -47
- package/dist/index.mjs.map +1 -1
- package/package.json +63 -63
- package/static/assets/index-CJCpPtrr.js +11 -0
- package/static/assets/index-ChbLMdDF.css +1 -0
- package/static/index.html +2 -2
- 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,11 +50,17 @@ __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,
|
|
@@ -95,7 +113,15 @@ var RingBuffer = class {
|
|
|
95
113
|
};
|
|
96
114
|
|
|
97
115
|
// src/safety.ts
|
|
98
|
-
var SECRET_KEY_PATTERN = /token|key|secret|
|
|
116
|
+
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;
|
|
117
|
+
var TEXT_PAIR_PATTERN = /\b([\w.-]*(?:token|secret|passw(?:or)?d|api[_-]?key|credential|authorization|cookie|dsn)[\w.-]*["']?)(\s*[=:]\s*)("[^"]*"|'[^']*'|[^\s,;&"'}]+)/gi;
|
|
118
|
+
var URL_CREDENTIALS_PATTERN = /([a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:)[^\s@/]+(@)/gi;
|
|
119
|
+
var BEARER_PATTERN = /\b(Bearer|Basic)\s+[A-Za-z0-9\-._~+/]+=*/gi;
|
|
120
|
+
var JWT_PATTERN = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]*/g;
|
|
121
|
+
function maskSecretText(text) {
|
|
122
|
+
if (text.length < 6) return text;
|
|
123
|
+
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}`);
|
|
124
|
+
}
|
|
99
125
|
function resolveActivation(env) {
|
|
100
126
|
if (env.NODEUI_ENABLED === "true") {
|
|
101
127
|
return { active: true, reason: "activated by NODEUI_ENABLED=true" };
|
|
@@ -118,7 +144,55 @@ function isLoopbackAddress(address) {
|
|
|
118
144
|
if (!address) return false;
|
|
119
145
|
return address.startsWith("127.") || address === "::1" || address.startsWith("::ffff:127.") || address === "localhost";
|
|
120
146
|
}
|
|
147
|
+
function normalizeIp(address) {
|
|
148
|
+
return address.startsWith("::ffff:") ? address.slice(7) : address;
|
|
149
|
+
}
|
|
150
|
+
function ipv4ToInt(ip) {
|
|
151
|
+
const parts = ip.split(".");
|
|
152
|
+
if (parts.length !== 4) return null;
|
|
153
|
+
let out = 0;
|
|
154
|
+
for (const part of parts) {
|
|
155
|
+
const n = Number(part);
|
|
156
|
+
if (!Number.isInteger(n) || n < 0 || n > 255) return null;
|
|
157
|
+
out = out * 256 + n;
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
function matchesAddress(address, allowed) {
|
|
162
|
+
if (!address) return false;
|
|
163
|
+
const ip = normalizeIp(address);
|
|
164
|
+
for (const entry of allowed) {
|
|
165
|
+
if (!entry.includes("/")) {
|
|
166
|
+
if (normalizeIp(entry) === ip) return true;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const [base, bitsRaw] = entry.split("/");
|
|
170
|
+
const bits = Number(bitsRaw);
|
|
171
|
+
const a = ipv4ToInt(ip);
|
|
172
|
+
const b = ipv4ToInt(base ?? "");
|
|
173
|
+
if (a === null || b === null || !Number.isInteger(bits) || bits < 0 || bits > 32) continue;
|
|
174
|
+
const mask = bits === 0 ? 0 : 4294967295 << 32 - bits >>> 0;
|
|
175
|
+
if ((a & mask) >>> 0 === (b & mask) >>> 0) return true;
|
|
176
|
+
}
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
function hostnameFromHostHeader(value) {
|
|
180
|
+
if (!value) return null;
|
|
181
|
+
const trimmed = value.trim().toLowerCase();
|
|
182
|
+
if (trimmed.startsWith("[")) {
|
|
183
|
+
const end = trimmed.indexOf("]");
|
|
184
|
+
return end === -1 ? null : trimmed.slice(1, end);
|
|
185
|
+
}
|
|
186
|
+
return trimmed.split(":")[0] || null;
|
|
187
|
+
}
|
|
188
|
+
function isLoopbackHostname(hostname) {
|
|
189
|
+
if (!hostname) return false;
|
|
190
|
+
return hostname === "localhost" || hostname.endsWith(".localhost") || isLoopbackAddress(hostname);
|
|
191
|
+
}
|
|
121
192
|
function maskSecrets(value, pattern = SECRET_KEY_PATTERN) {
|
|
193
|
+
if (typeof value === "string") {
|
|
194
|
+
return maskSecretText(value);
|
|
195
|
+
}
|
|
122
196
|
if (value !== null && typeof value === "object") {
|
|
123
197
|
if (Array.isArray(value)) {
|
|
124
198
|
return value.map((v) => maskSecrets(v, pattern));
|
|
@@ -142,17 +216,133 @@ function maskSecrets(value, pattern = SECRET_KEY_PATTERN) {
|
|
|
142
216
|
return value;
|
|
143
217
|
}
|
|
144
218
|
|
|
145
|
-
// src/
|
|
219
|
+
// src/guard.ts
|
|
146
220
|
var import_node_crypto = require("crypto");
|
|
221
|
+
var AUTH_COOKIE = "nodeui_token";
|
|
222
|
+
var FORWARD_HEADERS = ["x-forwarded-for", "x-forwarded-host", "forwarded", "x-real-ip"];
|
|
223
|
+
function digest(value) {
|
|
224
|
+
return (0, import_node_crypto.createHash)("sha256").update(value).digest();
|
|
225
|
+
}
|
|
226
|
+
function tokenEquals(a, b) {
|
|
227
|
+
return (0, import_node_crypto.timingSafeEqual)(digest(a), digest(b));
|
|
228
|
+
}
|
|
229
|
+
function readCookie(header, name) {
|
|
230
|
+
if (!header) return void 0;
|
|
231
|
+
for (const part of header.split(";")) {
|
|
232
|
+
const [k, ...rest] = part.trim().split("=");
|
|
233
|
+
if (k === name) {
|
|
234
|
+
try {
|
|
235
|
+
return decodeURIComponent(rest.join("="));
|
|
236
|
+
} catch {
|
|
237
|
+
return void 0;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return void 0;
|
|
242
|
+
}
|
|
243
|
+
function firstHeader(value) {
|
|
244
|
+
return Array.isArray(value) ? value[0] : value;
|
|
245
|
+
}
|
|
246
|
+
function deny(status, code, message) {
|
|
247
|
+
return { ok: false, status, code, message };
|
|
248
|
+
}
|
|
249
|
+
function createGuard(options) {
|
|
250
|
+
const strict = isLoopbackAddress(options.host);
|
|
251
|
+
const allowedHosts = options.allowedHosts.map((h) => h.toLowerCase());
|
|
252
|
+
const allowedOrigins = options.allowedOrigins.map((o) => o.toLowerCase().replace(/\/+$/, ""));
|
|
253
|
+
return (req) => {
|
|
254
|
+
const remote = req.socket.remoteAddress;
|
|
255
|
+
const remoteLoopback = isLoopbackAddress(remote);
|
|
256
|
+
const remoteAllowed = remoteLoopback || matchesAddress(remote, options.allowedRemoteAddresses);
|
|
257
|
+
if (strict && !remoteAllowed) {
|
|
258
|
+
return deny(
|
|
259
|
+
403,
|
|
260
|
+
"forbidden",
|
|
261
|
+
"nodeui is loopback-only; requests from other hosts are rejected"
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
if (!strict && options.allowedRemoteAddresses.length > 0 && !remoteAllowed) {
|
|
265
|
+
return deny(403, "forbidden", "remote address is not in the nodeui allow-list");
|
|
266
|
+
}
|
|
267
|
+
if (strict && !options.trustProxy && !matchesAddress(remote, options.allowedRemoteAddresses)) {
|
|
268
|
+
if (FORWARD_HEADERS.some((h) => req.headers[h] !== void 0)) {
|
|
269
|
+
return deny(
|
|
270
|
+
403,
|
|
271
|
+
"forbidden",
|
|
272
|
+
"proxied requests are rejected (set trustProxy to allow forwarded traffic)"
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const hostHeader = firstHeader(req.headers.host);
|
|
277
|
+
const hostname = hostnameFromHostHeader(hostHeader);
|
|
278
|
+
if (hostname !== null) {
|
|
279
|
+
const hostOk = isLoopbackHostname(hostname) || allowedHosts.includes(hostname) || !strict && allowedHosts.length === 0;
|
|
280
|
+
if (!hostOk) {
|
|
281
|
+
return deny(
|
|
282
|
+
403,
|
|
283
|
+
"forbidden",
|
|
284
|
+
`Host "${hostname}" is not allowed (add it to allowedHosts to use nodeui via this name)`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const origin = firstHeader(req.headers.origin);
|
|
289
|
+
if (origin !== void 0) {
|
|
290
|
+
let originOk = false;
|
|
291
|
+
const normalized = origin.toLowerCase().replace(/\/+$/, "");
|
|
292
|
+
if (allowedOrigins.includes(normalized)) {
|
|
293
|
+
originOk = true;
|
|
294
|
+
} else {
|
|
295
|
+
try {
|
|
296
|
+
originOk = hostHeader !== void 0 && new URL(origin).host === hostHeader.toLowerCase();
|
|
297
|
+
} catch {
|
|
298
|
+
originOk = false;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (!originOk) {
|
|
302
|
+
return deny(403, "forbidden", "cross-origin requests to nodeui are rejected");
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (options.authToken) {
|
|
306
|
+
const authHeader = firstHeader(req.headers.authorization);
|
|
307
|
+
const bearer = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : void 0;
|
|
308
|
+
const queryToken = new URL(req.url ?? "/", "http://localhost").searchParams.get("token");
|
|
309
|
+
const cookieToken = readCookie(firstHeader(req.headers.cookie), AUTH_COOKIE);
|
|
310
|
+
const candidates = [
|
|
311
|
+
bearer,
|
|
312
|
+
firstHeader(req.headers["x-nodeui-token"]),
|
|
313
|
+
cookieToken,
|
|
314
|
+
queryToken ?? void 0
|
|
315
|
+
];
|
|
316
|
+
const matched = candidates.find((c) => c !== void 0 && tokenEquals(c, options.authToken));
|
|
317
|
+
if (matched === void 0) {
|
|
318
|
+
return deny(401, "unauthorized", "a valid nodeui token is required");
|
|
319
|
+
}
|
|
320
|
+
if (queryToken !== null && matched === queryToken) {
|
|
321
|
+
return { ok: true, setToken: queryToken };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return { ok: true };
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// src/confirmations.ts
|
|
329
|
+
var import_node_crypto2 = require("crypto");
|
|
147
330
|
var ConfirmationStore = class {
|
|
148
|
-
constructor(ttlMs) {
|
|
331
|
+
constructor(ttlMs, maxOutstanding = 100) {
|
|
149
332
|
this.ttlMs = ttlMs;
|
|
333
|
+
this.maxOutstanding = maxOutstanding;
|
|
150
334
|
}
|
|
151
335
|
ttlMs;
|
|
336
|
+
maxOutstanding;
|
|
152
337
|
nonces = /* @__PURE__ */ new Map();
|
|
153
338
|
issue() {
|
|
154
339
|
this.prune();
|
|
155
|
-
|
|
340
|
+
while (this.nonces.size >= this.maxOutstanding) {
|
|
341
|
+
const oldest = this.nonces.keys().next().value;
|
|
342
|
+
if (oldest === void 0) break;
|
|
343
|
+
this.nonces.delete(oldest);
|
|
344
|
+
}
|
|
345
|
+
const nonce = (0, import_node_crypto2.randomBytes)(16).toString("hex");
|
|
156
346
|
const expiresAtMs = Date.now() + this.ttlMs;
|
|
157
347
|
this.nonces.set(nonce, expiresAtMs);
|
|
158
348
|
return { nonce, expiresAtMs, ttlMs: this.ttlMs };
|
|
@@ -464,15 +654,65 @@ var EventLoopLagProvider = class {
|
|
|
464
654
|
// src/providers/health.ts
|
|
465
655
|
var LAG_OK_MS = 50;
|
|
466
656
|
var LAG_DEGRADED_MS = 200;
|
|
657
|
+
var CHECK_TIMEOUT_MS = 3e3;
|
|
658
|
+
var CHECK_CACHE_MS = 5e3;
|
|
659
|
+
function withTimeout(promise, ms) {
|
|
660
|
+
return new Promise((resolve3, reject) => {
|
|
661
|
+
const timer = setTimeout(() => reject(new Error(`timed out after ${ms}ms`)), ms);
|
|
662
|
+
promise.then(
|
|
663
|
+
(value) => {
|
|
664
|
+
clearTimeout(timer);
|
|
665
|
+
resolve3(value);
|
|
666
|
+
},
|
|
667
|
+
(err) => {
|
|
668
|
+
clearTimeout(timer);
|
|
669
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
670
|
+
}
|
|
671
|
+
);
|
|
672
|
+
});
|
|
673
|
+
}
|
|
467
674
|
var HealthProvider = class {
|
|
675
|
+
constructor(checks = {}) {
|
|
676
|
+
this.checks = checks;
|
|
677
|
+
}
|
|
678
|
+
checks;
|
|
468
679
|
id = "health";
|
|
469
|
-
|
|
680
|
+
cache = null;
|
|
681
|
+
async runChecks() {
|
|
682
|
+
const names = Object.keys(this.checks);
|
|
683
|
+
if (names.length === 0) return [];
|
|
684
|
+
if (this.cache && Date.now() - this.cache.at < CHECK_CACHE_MS) return this.cache.results;
|
|
685
|
+
const results = await Promise.all(
|
|
686
|
+
names.map(async (name) => {
|
|
687
|
+
const started = Date.now();
|
|
688
|
+
try {
|
|
689
|
+
const outcome = await withTimeout(
|
|
690
|
+
Promise.resolve().then(() => this.checks[name]()),
|
|
691
|
+
CHECK_TIMEOUT_MS
|
|
692
|
+
);
|
|
693
|
+
if (outcome === false) throw new Error("check returned false");
|
|
694
|
+
return { name, status: "up", durationMs: Date.now() - started };
|
|
695
|
+
} catch (err) {
|
|
696
|
+
return {
|
|
697
|
+
name,
|
|
698
|
+
status: "down",
|
|
699
|
+
durationMs: Date.now() - started,
|
|
700
|
+
error: err instanceof Error ? err.message : String(err)
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
})
|
|
704
|
+
);
|
|
705
|
+
this.cache = { at: Date.now(), results };
|
|
706
|
+
return results;
|
|
707
|
+
}
|
|
708
|
+
async get(ctx) {
|
|
470
709
|
const loop = ctx.store["event-loop"];
|
|
471
710
|
const memory = ctx.store.memory;
|
|
472
711
|
const lagMs = typeof loop?.currentMs === "number" ? loop.currentMs : null;
|
|
473
712
|
const totalMem = typeof memory?.totalMem === "number" ? memory.totalMem : 0;
|
|
474
713
|
const rss = typeof memory?.rss === "number" ? memory.rss : 0;
|
|
475
714
|
const memoryUsedPercent = totalMem > 0 ? rss / totalMem * 100 : null;
|
|
715
|
+
const checks = await this.runChecks();
|
|
476
716
|
let status;
|
|
477
717
|
let statusReason;
|
|
478
718
|
if (lagMs === null) {
|
|
@@ -488,6 +728,11 @@ var HealthProvider = class {
|
|
|
488
728
|
status = "critical";
|
|
489
729
|
statusReason = `event-loop lag ${lagMs.toFixed(1)}ms above ${LAG_DEGRADED_MS}ms`;
|
|
490
730
|
}
|
|
731
|
+
const down = checks.filter((c) => c.status === "down");
|
|
732
|
+
if (down.length > 0) {
|
|
733
|
+
status = "critical";
|
|
734
|
+
statusReason = `dependency down: ${down.map((c) => c.name).join(", ")}`;
|
|
735
|
+
}
|
|
491
736
|
return {
|
|
492
737
|
ok: true,
|
|
493
738
|
data: {
|
|
@@ -498,19 +743,202 @@ var HealthProvider = class {
|
|
|
498
743
|
nodeVersion: process.version,
|
|
499
744
|
platform: process.platform,
|
|
500
745
|
eventLoopLagMs: lagMs,
|
|
501
|
-
memoryUsedPercent
|
|
746
|
+
memoryUsedPercent,
|
|
747
|
+
checks
|
|
502
748
|
}
|
|
503
749
|
};
|
|
504
750
|
}
|
|
505
751
|
};
|
|
506
752
|
|
|
753
|
+
// src/providers/outgoing.ts
|
|
754
|
+
var import_node_diagnostics_channel = __toESM(require("diagnostics_channel"));
|
|
755
|
+
var import_node_http = __toESM(require("http"));
|
|
756
|
+
var import_node_https = __toESM(require("https"));
|
|
757
|
+
var import_node_module = require("module");
|
|
758
|
+
var RESPONSE_PAGE_SIZE = 100;
|
|
759
|
+
var recorders = /* @__PURE__ */ new Set();
|
|
760
|
+
var originals = /* @__PURE__ */ new Map();
|
|
761
|
+
var undiciUnsubscribe = null;
|
|
762
|
+
function describeTarget(defaultProtocol, args) {
|
|
763
|
+
try {
|
|
764
|
+
let method = "GET";
|
|
765
|
+
let host = "unknown";
|
|
766
|
+
let path = "/";
|
|
767
|
+
const [first, second] = args;
|
|
768
|
+
const opts = typeof first === "string" || first instanceof URL ? second : first;
|
|
769
|
+
if (typeof first === "string" || first instanceof URL) {
|
|
770
|
+
const parsed = new URL(first.toString());
|
|
771
|
+
host = parsed.host;
|
|
772
|
+
path = parsed.pathname;
|
|
773
|
+
}
|
|
774
|
+
if (opts && typeof opts === "object") {
|
|
775
|
+
if (typeof opts.method === "string") method = opts.method.toUpperCase();
|
|
776
|
+
if (typeof first !== "string" && !(first instanceof URL)) {
|
|
777
|
+
const name = opts.hostname ?? opts.host ?? "localhost";
|
|
778
|
+
host = opts.port ? `${name}:${String(opts.port)}` : name;
|
|
779
|
+
path = typeof opts.path === "string" ? opts.path.split("?")[0] ?? "/" : "/";
|
|
780
|
+
const protocol = typeof opts.protocol === "string" ? opts.protocol : defaultProtocol;
|
|
781
|
+
return { method, url: `${protocol}//${host}${path}` };
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
return { method, url: `${defaultProtocol}//${host}${path}` };
|
|
785
|
+
} catch {
|
|
786
|
+
return null;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
function emit(entry) {
|
|
790
|
+
for (const record of recorders) {
|
|
791
|
+
try {
|
|
792
|
+
record(entry);
|
|
793
|
+
} catch {
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
function wrapModule(mod, protocol) {
|
|
798
|
+
for (const name of ["request", "get"]) {
|
|
799
|
+
const key = `${protocol}${name}`;
|
|
800
|
+
const original = mod[name];
|
|
801
|
+
originals.set(key, original);
|
|
802
|
+
const wrapped = function(...args) {
|
|
803
|
+
const req = original.apply(this, args);
|
|
804
|
+
const target = recorders.size > 0 ? describeTarget(protocol, args) : null;
|
|
805
|
+
if (target && req && typeof req.once === "function") {
|
|
806
|
+
const started = process.hrtime.bigint();
|
|
807
|
+
const timestampMs = Date.now();
|
|
808
|
+
let done = false;
|
|
809
|
+
const finish = (status, error) => {
|
|
810
|
+
if (done) return;
|
|
811
|
+
done = true;
|
|
812
|
+
emit({
|
|
813
|
+
...target,
|
|
814
|
+
status,
|
|
815
|
+
durationMs: Number(process.hrtime.bigint() - started) / 1e6,
|
|
816
|
+
timestampMs,
|
|
817
|
+
...error ? { error } : {}
|
|
818
|
+
});
|
|
819
|
+
};
|
|
820
|
+
req.once("response", (res) => {
|
|
821
|
+
const status = res.statusCode ?? null;
|
|
822
|
+
res.once("end", () => finish(status));
|
|
823
|
+
res.once("close", () => finish(status));
|
|
824
|
+
});
|
|
825
|
+
req.once("error", (err) => finish(null, err.message));
|
|
826
|
+
}
|
|
827
|
+
return req;
|
|
828
|
+
};
|
|
829
|
+
mod[name] = wrapped;
|
|
830
|
+
}
|
|
831
|
+
(0, import_node_module.syncBuiltinESMExports)();
|
|
832
|
+
}
|
|
833
|
+
function unwrapModule(mod, protocol) {
|
|
834
|
+
for (const name of ["request", "get"]) {
|
|
835
|
+
const original = originals.get(`${protocol}${name}`);
|
|
836
|
+
if (original) mod[name] = original;
|
|
837
|
+
originals.delete(`${protocol}${name}`);
|
|
838
|
+
}
|
|
839
|
+
(0, import_node_module.syncBuiltinESMExports)();
|
|
840
|
+
}
|
|
841
|
+
function subscribeUndici() {
|
|
842
|
+
const started = /* @__PURE__ */ new WeakMap();
|
|
843
|
+
const statuses = /* @__PURE__ */ new WeakMap();
|
|
844
|
+
const target = (req) => ({
|
|
845
|
+
method: (req.method ?? "GET").toUpperCase(),
|
|
846
|
+
url: `${String(req.origin ?? "")}${(req.path ?? "/").split("?")[0]}`
|
|
847
|
+
});
|
|
848
|
+
const onCreate = (message) => {
|
|
849
|
+
const request = message.request;
|
|
850
|
+
if (request) started.set(request, { at: process.hrtime.bigint(), ts: Date.now() });
|
|
851
|
+
};
|
|
852
|
+
const onHeaders = (message) => {
|
|
853
|
+
const { request, response } = message;
|
|
854
|
+
if (request && typeof response?.statusCode === "number") {
|
|
855
|
+
statuses.set(request, response.statusCode);
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
const finish = (request, error) => {
|
|
859
|
+
if (!request) return;
|
|
860
|
+
const begin = started.get(request);
|
|
861
|
+
if (!begin) return;
|
|
862
|
+
started.delete(request);
|
|
863
|
+
emit({
|
|
864
|
+
...target(request),
|
|
865
|
+
status: statuses.get(request) ?? null,
|
|
866
|
+
durationMs: Number(process.hrtime.bigint() - begin.at) / 1e6,
|
|
867
|
+
timestampMs: begin.ts,
|
|
868
|
+
...error ? { error } : {}
|
|
869
|
+
});
|
|
870
|
+
};
|
|
871
|
+
const onTrailers = (message) => finish(message.request);
|
|
872
|
+
const onError = (message) => {
|
|
873
|
+
const { request, error } = message;
|
|
874
|
+
finish(request, error?.message ?? "request failed");
|
|
875
|
+
};
|
|
876
|
+
import_node_diagnostics_channel.default.subscribe("undici:request:create", onCreate);
|
|
877
|
+
import_node_diagnostics_channel.default.subscribe("undici:request:headers", onHeaders);
|
|
878
|
+
import_node_diagnostics_channel.default.subscribe("undici:request:trailers", onTrailers);
|
|
879
|
+
import_node_diagnostics_channel.default.subscribe("undici:request:error", onError);
|
|
880
|
+
return () => {
|
|
881
|
+
import_node_diagnostics_channel.default.unsubscribe("undici:request:create", onCreate);
|
|
882
|
+
import_node_diagnostics_channel.default.unsubscribe("undici:request:headers", onHeaders);
|
|
883
|
+
import_node_diagnostics_channel.default.unsubscribe("undici:request:trailers", onTrailers);
|
|
884
|
+
import_node_diagnostics_channel.default.unsubscribe("undici:request:error", onError);
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
function interceptOutgoing(record) {
|
|
888
|
+
const first = recorders.size === 0;
|
|
889
|
+
recorders.add(record);
|
|
890
|
+
if (first) {
|
|
891
|
+
wrapModule(import_node_http.default, "http:");
|
|
892
|
+
wrapModule(import_node_https.default, "https:");
|
|
893
|
+
try {
|
|
894
|
+
undiciUnsubscribe = subscribeUndici();
|
|
895
|
+
} catch {
|
|
896
|
+
undiciUnsubscribe = null;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return () => {
|
|
900
|
+
if (!recorders.delete(record) || recorders.size > 0) return;
|
|
901
|
+
unwrapModule(import_node_http.default, "http:");
|
|
902
|
+
unwrapModule(import_node_https.default, "https:");
|
|
903
|
+
undiciUnsubscribe?.();
|
|
904
|
+
undiciUnsubscribe = null;
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
var OutgoingProvider = class {
|
|
908
|
+
id = "outgoing";
|
|
909
|
+
buffer;
|
|
910
|
+
nextId = 1;
|
|
911
|
+
failed = 0;
|
|
912
|
+
release = null;
|
|
913
|
+
constructor(size) {
|
|
914
|
+
this.buffer = new RingBuffer(size);
|
|
915
|
+
}
|
|
916
|
+
start() {
|
|
917
|
+
if (this.release) return;
|
|
918
|
+
this.release = interceptOutgoing((entry) => this.record(entry));
|
|
919
|
+
}
|
|
920
|
+
stop() {
|
|
921
|
+
this.release?.();
|
|
922
|
+
this.release = null;
|
|
923
|
+
}
|
|
924
|
+
record(entry) {
|
|
925
|
+
this.buffer.push({ id: this.nextId, ...entry });
|
|
926
|
+
this.nextId += 1;
|
|
927
|
+
if (entry.error || entry.status !== null && entry.status >= 500) this.failed += 1;
|
|
928
|
+
}
|
|
929
|
+
get() {
|
|
930
|
+
const entries = this.buffer.slice(Math.max(0, this.buffer.length - RESPONSE_PAGE_SIZE));
|
|
931
|
+
return { ok: true, data: { total: this.nextId - 1, failed: this.failed, entries } };
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
|
|
507
935
|
// src/providers/heap-snapshot.ts
|
|
508
936
|
var import_promises = require("fs/promises");
|
|
509
937
|
var import_node_os2 = require("os");
|
|
510
938
|
var import_node_path2 = require("path");
|
|
511
939
|
var import_node_v8 = require("v8");
|
|
512
940
|
function defaultSnapshotDir() {
|
|
513
|
-
return process.env.NODEUI_HEAP_SNAPSHOT_DIR ?? (0, import_node_os2.tmpdir)();
|
|
941
|
+
return process.env.NODEUI_HEAP_SNAPSHOT_DIR ?? (0, import_node_path2.join)((0, import_node_os2.tmpdir)(), "nodeui-heap");
|
|
514
942
|
}
|
|
515
943
|
var HeapSnapshotProvider = class {
|
|
516
944
|
id = "heap-snapshot";
|
|
@@ -524,8 +952,9 @@ var HeapSnapshotProvider = class {
|
|
|
524
952
|
const fileName = `nodeui-heap-${process.pid}-${stamp}.heapsnapshot`;
|
|
525
953
|
const filePath = (0, import_node_path2.join)(dir, fileName);
|
|
526
954
|
try {
|
|
527
|
-
await (0, import_promises.mkdir)(dir, { recursive: true });
|
|
955
|
+
await (0, import_promises.mkdir)(dir, { recursive: true, mode: 448 });
|
|
528
956
|
(0, import_node_v8.writeHeapSnapshot)(filePath);
|
|
957
|
+
await (0, import_promises.chmod)(filePath, 384).catch(() => void 0);
|
|
529
958
|
const fileStat = await (0, import_promises.stat)(filePath);
|
|
530
959
|
const data = {
|
|
531
960
|
fileName,
|
|
@@ -548,7 +977,7 @@ var HeapSnapshotProvider = class {
|
|
|
548
977
|
};
|
|
549
978
|
|
|
550
979
|
// src/providers/requests.ts
|
|
551
|
-
var
|
|
980
|
+
var RESPONSE_PAGE_SIZE2 = 100;
|
|
552
981
|
var RequestsProvider = class {
|
|
553
982
|
id = "requests";
|
|
554
983
|
buffer;
|
|
@@ -564,7 +993,7 @@ var RequestsProvider = class {
|
|
|
564
993
|
return this.buffer.length;
|
|
565
994
|
}
|
|
566
995
|
get() {
|
|
567
|
-
const entries = this.buffer.slice(Math.max(0, this.buffer.length -
|
|
996
|
+
const entries = this.buffer.slice(Math.max(0, this.buffer.length - RESPONSE_PAGE_SIZE2));
|
|
568
997
|
return { ok: true, data: { total: this.buffer.length, entries } };
|
|
569
998
|
}
|
|
570
999
|
};
|
|
@@ -679,6 +1108,11 @@ var RoutesProvider = class {
|
|
|
679
1108
|
id = "routes";
|
|
680
1109
|
cache = /* @__PURE__ */ new WeakMap();
|
|
681
1110
|
get(ctx) {
|
|
1111
|
+
const source = ctx.store["routes-source"];
|
|
1112
|
+
if (source !== void 0) {
|
|
1113
|
+
const routes2 = typeof source === "function" ? source() : source;
|
|
1114
|
+
return { ok: true, data: { routes: routes2 } };
|
|
1115
|
+
}
|
|
682
1116
|
const router = ctx.store["express-router"];
|
|
683
1117
|
if (!router) {
|
|
684
1118
|
return {
|
|
@@ -727,14 +1161,14 @@ function formatArg(arg) {
|
|
|
727
1161
|
var refs = 0;
|
|
728
1162
|
var pushes = /* @__PURE__ */ new Set();
|
|
729
1163
|
var wrappers = /* @__PURE__ */ new Map();
|
|
730
|
-
var
|
|
1164
|
+
var originals2 = {};
|
|
731
1165
|
function consoleRef() {
|
|
732
1166
|
return console;
|
|
733
1167
|
}
|
|
734
1168
|
function installWrappers() {
|
|
735
1169
|
for (const [method, level] of CONSOLE_METHODS) {
|
|
736
1170
|
const original = consoleRef()[method];
|
|
737
|
-
|
|
1171
|
+
originals2[method] = original;
|
|
738
1172
|
const wrapper = (...args) => {
|
|
739
1173
|
for (const push of pushes) {
|
|
740
1174
|
try {
|
|
@@ -752,7 +1186,7 @@ function uninstallWrappers() {
|
|
|
752
1186
|
for (const [method] of CONSOLE_METHODS) {
|
|
753
1187
|
const current = consoleRef()[method];
|
|
754
1188
|
if (current === wrappers.get(method)) {
|
|
755
|
-
const original =
|
|
1189
|
+
const original = originals2[method];
|
|
756
1190
|
if (original) {
|
|
757
1191
|
consoleRef()[method] = original;
|
|
758
1192
|
} else {
|
|
@@ -761,7 +1195,7 @@ function uninstallWrappers() {
|
|
|
761
1195
|
}
|
|
762
1196
|
}
|
|
763
1197
|
wrappers.clear();
|
|
764
|
-
|
|
1198
|
+
originals2 = {};
|
|
765
1199
|
}
|
|
766
1200
|
function interceptConsole(push) {
|
|
767
1201
|
refs += 1;
|
|
@@ -808,9 +1242,10 @@ var LogsProvider = class {
|
|
|
808
1242
|
};
|
|
809
1243
|
|
|
810
1244
|
// src/sse.ts
|
|
811
|
-
function startSse(res) {
|
|
1245
|
+
function startSse(res, extraHeaders = {}) {
|
|
812
1246
|
let closed = false;
|
|
813
1247
|
res.writeHead(200, {
|
|
1248
|
+
...extraHeaders,
|
|
814
1249
|
"Content-Type": "text/event-stream",
|
|
815
1250
|
"Cache-Control": "no-cache",
|
|
816
1251
|
Connection: "keep-alive",
|
|
@@ -841,8 +1276,35 @@ var import_node_os3 = require("os");
|
|
|
841
1276
|
var import_node_path3 = require("path");
|
|
842
1277
|
var STATIC_ROOT = (0, import_node_path3.resolve)(__dirname, "..", "static");
|
|
843
1278
|
var SSE_HEARTBEAT_MS = 15e3;
|
|
844
|
-
function serializeEnvelope(envelope) {
|
|
845
|
-
return JSON.stringify(maskSecrets(envelope));
|
|
1279
|
+
function serializeEnvelope(envelope, mask = true) {
|
|
1280
|
+
return JSON.stringify(mask ? maskSecrets(envelope) : envelope);
|
|
1281
|
+
}
|
|
1282
|
+
var BUILT_IN_PANELS = /* @__PURE__ */ new Set([
|
|
1283
|
+
"health",
|
|
1284
|
+
"memory",
|
|
1285
|
+
"cpu",
|
|
1286
|
+
"event-loop",
|
|
1287
|
+
"heap-snapshot",
|
|
1288
|
+
"startup",
|
|
1289
|
+
"requests",
|
|
1290
|
+
"env",
|
|
1291
|
+
"routes",
|
|
1292
|
+
"logs",
|
|
1293
|
+
"metrics",
|
|
1294
|
+
"outgoing"
|
|
1295
|
+
]);
|
|
1296
|
+
var PLUGIN_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
1297
|
+
var RESERVED_API_NAMES = /* @__PURE__ */ new Set(["config", "live", "confirmations"]);
|
|
1298
|
+
var SECURITY_HEADERS = {
|
|
1299
|
+
"X-Content-Type-Options": "nosniff",
|
|
1300
|
+
"X-Frame-Options": "DENY",
|
|
1301
|
+
"Referrer-Policy": "no-referrer",
|
|
1302
|
+
"Cross-Origin-Resource-Policy": "same-origin"
|
|
1303
|
+
};
|
|
1304
|
+
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'";
|
|
1305
|
+
function listFrom(value, raw) {
|
|
1306
|
+
if (value) return value;
|
|
1307
|
+
return (raw ?? "").split(",").map((v) => v.trim()).filter(Boolean);
|
|
846
1308
|
}
|
|
847
1309
|
function pathnameOf(req) {
|
|
848
1310
|
const raw = req.originalUrl ?? req.url ?? "/";
|
|
@@ -902,19 +1364,46 @@ function createNodeUI(options = {}) {
|
|
|
902
1364
|
env.NODEUI_CONFIRM_TTL_MS ? Number(env.NODEUI_CONFIRM_TTL_MS) : 6e4,
|
|
903
1365
|
"NODEUI_CONFIRM_TTL_MS"
|
|
904
1366
|
),
|
|
905
|
-
heapSnapshotDir: options.heapSnapshotDir ?? env.NODEUI_HEAP_SNAPSHOT_DIR ?? (0, import_node_os3.tmpdir)()
|
|
1367
|
+
heapSnapshotDir: options.heapSnapshotDir ?? env.NODEUI_HEAP_SNAPSHOT_DIR ?? (0, import_node_path3.join)((0, import_node_os3.tmpdir)(), "nodeui-heap"),
|
|
1368
|
+
allowedHosts: listFrom(options.allowedHosts, env.NODEUI_ALLOWED_HOSTS),
|
|
1369
|
+
allowedOrigins: listFrom(options.allowedOrigins, env.NODEUI_ALLOWED_ORIGINS),
|
|
1370
|
+
allowedRemoteAddresses: listFrom(options.allowedRemoteAddresses, env.NODEUI_ALLOWED_REMOTE),
|
|
1371
|
+
trustProxy: options.trustProxy ?? env.NODEUI_TRUST_PROXY === "true",
|
|
1372
|
+
authRequired: Boolean(options.authToken ?? env.NODEUI_TOKEN),
|
|
1373
|
+
maxSseClients: positiveInt(
|
|
1374
|
+
options.maxSseClients,
|
|
1375
|
+
env.NODEUI_MAX_SSE_CLIENTS ? Number(env.NODEUI_MAX_SSE_CLIENTS) : 10,
|
|
1376
|
+
"NODEUI_MAX_SSE_CLIENTS"
|
|
1377
|
+
)
|
|
906
1378
|
};
|
|
907
|
-
|
|
1379
|
+
const authToken = options.authToken ?? env.NODEUI_TOKEN;
|
|
1380
|
+
const guard = createGuard({
|
|
1381
|
+
host: config.host,
|
|
1382
|
+
allowedHosts: config.allowedHosts,
|
|
1383
|
+
allowedOrigins: config.allowedOrigins,
|
|
1384
|
+
allowedRemoteAddresses: config.allowedRemoteAddresses,
|
|
1385
|
+
trustProxy: config.trustProxy,
|
|
1386
|
+
authToken: authToken || void 0
|
|
1387
|
+
});
|
|
1388
|
+
if (config.enabled && !isLoopbackAddress(config.host)) {
|
|
908
1389
|
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
|
|
1390
|
+
`[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
1391
|
);
|
|
911
1392
|
}
|
|
1393
|
+
if (config.enabled && env.NODE_ENV === "production") {
|
|
1394
|
+
console.warn(
|
|
1395
|
+
"[nodeui] running with NODE_ENV=production. The console exposes environment, logs and heap data; keep it behind loopback and an auth token."
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
if (config.enabled && !config.maskSecrets) {
|
|
1399
|
+
console.warn("[nodeui] secret masking is disabled; panels may show credentials.");
|
|
1400
|
+
}
|
|
912
1401
|
const ctx = { config, env, store: {} };
|
|
913
1402
|
if (options.config !== void 0) {
|
|
914
1403
|
ctx.store["app-config"] = options.config;
|
|
915
1404
|
}
|
|
916
1405
|
const registry = new ProviderRegistry();
|
|
917
|
-
registry.register(new HealthProvider());
|
|
1406
|
+
registry.register(new HealthProvider(options.healthChecks));
|
|
918
1407
|
registry.register(new MemoryProvider());
|
|
919
1408
|
registry.register(new CpuProvider());
|
|
920
1409
|
registry.register(new EventLoopLagProvider());
|
|
@@ -936,9 +1425,23 @@ function createNodeUI(options = {}) {
|
|
|
936
1425
|
registry.register(envProvider);
|
|
937
1426
|
const routesProvider = new RoutesProvider();
|
|
938
1427
|
registry.register(routesProvider);
|
|
1428
|
+
registry.register(
|
|
1429
|
+
new OutgoingProvider(positiveInt(options.outgoingLogSize, 200, "outgoingLogSize"))
|
|
1430
|
+
);
|
|
1431
|
+
const pluginMeta = [];
|
|
1432
|
+
for (const plugin of options.plugins ?? []) {
|
|
1433
|
+
if (!PLUGIN_ID_PATTERN.test(plugin.id) || BUILT_IN_PANELS.has(plugin.id) || RESERVED_API_NAMES.has(plugin.id) || registry.get(plugin.id)) {
|
|
1434
|
+
throw new Error(
|
|
1435
|
+
`nodeui plugin id "${plugin.id}" is invalid or already taken (use lowercase letters, digits and "-")`
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
registry.register(plugin);
|
|
1439
|
+
pluginMeta.push({ id: plugin.id, title: plugin.title ?? plugin.id });
|
|
1440
|
+
}
|
|
939
1441
|
const confirmations = new ConfirmationStore(config.confirmTtlMs);
|
|
940
1442
|
const activeProviders = /* @__PURE__ */ new Set();
|
|
941
1443
|
let inactivityTimer = null;
|
|
1444
|
+
let sseClients = 0;
|
|
942
1445
|
function stopAll() {
|
|
943
1446
|
for (const id of activeProviders) {
|
|
944
1447
|
registry.get(id)?.stop?.(ctx);
|
|
@@ -962,8 +1465,9 @@ function createNodeUI(options = {}) {
|
|
|
962
1465
|
resetInactivityTimer();
|
|
963
1466
|
}
|
|
964
1467
|
function sendJson(res, status, envelope) {
|
|
965
|
-
const body = serializeEnvelope(envelope);
|
|
1468
|
+
const body = serializeEnvelope(envelope, config.maskSecrets);
|
|
966
1469
|
res.writeHead(status, {
|
|
1470
|
+
...SECURITY_HEADERS,
|
|
967
1471
|
"Content-Type": "application/json",
|
|
968
1472
|
"Content-Length": Buffer.byteLength(body),
|
|
969
1473
|
"Cache-Control": "no-store"
|
|
@@ -981,8 +1485,24 @@ function createNodeUI(options = {}) {
|
|
|
981
1485
|
const provider = registry.get(id);
|
|
982
1486
|
if (!provider) return notFound(res);
|
|
983
1487
|
const requestCtx = query ? { ...ctx, query } : ctx;
|
|
984
|
-
|
|
985
|
-
|
|
1488
|
+
sendJson(res, ...await readPanel(provider, requestCtx));
|
|
1489
|
+
}
|
|
1490
|
+
async function readPanel(provider, requestCtx) {
|
|
1491
|
+
try {
|
|
1492
|
+
const result = await provider.get(requestCtx);
|
|
1493
|
+
return [result.ok ? 200 : 500, result];
|
|
1494
|
+
} catch (err) {
|
|
1495
|
+
return [
|
|
1496
|
+
500,
|
|
1497
|
+
{
|
|
1498
|
+
ok: false,
|
|
1499
|
+
error: {
|
|
1500
|
+
code: "provider-failed",
|
|
1501
|
+
message: err instanceof Error ? err.message : "provider failed"
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
];
|
|
1505
|
+
}
|
|
986
1506
|
}
|
|
987
1507
|
function configData() {
|
|
988
1508
|
return {
|
|
@@ -995,7 +1515,9 @@ function createNodeUI(options = {}) {
|
|
|
995
1515
|
logSize: config.logSize,
|
|
996
1516
|
pollIntervalMs: config.pollIntervalMs,
|
|
997
1517
|
panels: registry.ids(),
|
|
998
|
-
masking: { enabled: config.maskSecrets, pattern: SECRET_KEY_PATTERN.source }
|
|
1518
|
+
masking: { enabled: config.maskSecrets, pattern: SECRET_KEY_PATTERN.source },
|
|
1519
|
+
authRequired: config.authRequired,
|
|
1520
|
+
plugins: pluginMeta
|
|
999
1521
|
};
|
|
1000
1522
|
}
|
|
1001
1523
|
function issueConfirmation(res) {
|
|
@@ -1027,22 +1549,37 @@ function createNodeUI(options = {}) {
|
|
|
1027
1549
|
const requested = (search.get("panels") ?? "").split(",").filter(Boolean);
|
|
1028
1550
|
const panels = (requested.length > 0 ? requested.filter((id) => registry.get(id) !== void 0) : registry.ids()).filter((value, index, self) => self.indexOf(value) === index);
|
|
1029
1551
|
if (panels.length === 0) return notFound(res);
|
|
1030
|
-
|
|
1552
|
+
if (sseClients >= config.maxSseClients) {
|
|
1553
|
+
res.writeHead(429, { ...SECURITY_HEADERS, "Content-Type": "application/json" });
|
|
1554
|
+
res.end(
|
|
1555
|
+
JSON.stringify({
|
|
1556
|
+
ok: false,
|
|
1557
|
+
error: { code: "too-many-streams", message: "too many open live streams" }
|
|
1558
|
+
})
|
|
1559
|
+
);
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
sseClients += 1;
|
|
1563
|
+
const stream = startSse(res, SECURITY_HEADERS);
|
|
1031
1564
|
const push = async () => {
|
|
1032
1565
|
for (const id of panels) {
|
|
1033
1566
|
ensureActive(id);
|
|
1034
1567
|
const provider = registry.get(id);
|
|
1035
1568
|
if (!provider) continue;
|
|
1036
|
-
const result = await provider
|
|
1037
|
-
stream.send({ panel: id, envelope: result });
|
|
1569
|
+
const [, result] = await readPanel(provider, ctx);
|
|
1570
|
+
stream.send({ panel: id, envelope: config.maskSecrets ? maskSecrets(result) : result });
|
|
1038
1571
|
}
|
|
1039
1572
|
};
|
|
1040
|
-
void push();
|
|
1041
|
-
const pushTimer = setInterval(() => void push(), config.pollIntervalMs);
|
|
1573
|
+
void push().catch(() => void 0);
|
|
1574
|
+
const pushTimer = setInterval(() => void push().catch(() => void 0), config.pollIntervalMs);
|
|
1042
1575
|
const heartbeatTimer = setInterval(() => stream.heartbeat(), SSE_HEARTBEAT_MS);
|
|
1043
1576
|
if (typeof pushTimer.unref === "function") pushTimer.unref();
|
|
1044
1577
|
if (typeof heartbeatTimer.unref === "function") heartbeatTimer.unref();
|
|
1578
|
+
let cleaned = false;
|
|
1045
1579
|
const cleanup = () => {
|
|
1580
|
+
if (cleaned) return;
|
|
1581
|
+
cleaned = true;
|
|
1582
|
+
sseClients -= 1;
|
|
1046
1583
|
clearInterval(pushTimer);
|
|
1047
1584
|
clearInterval(heartbeatTimer);
|
|
1048
1585
|
stream.close();
|
|
@@ -1085,30 +1622,33 @@ function createNodeUI(options = {}) {
|
|
|
1085
1622
|
return;
|
|
1086
1623
|
}
|
|
1087
1624
|
res.writeHead(200, {
|
|
1625
|
+
...SECURITY_HEADERS,
|
|
1626
|
+
"Content-Security-Policy": CSP,
|
|
1088
1627
|
"Content-Type": asset.contentType,
|
|
1089
1628
|
"Content-Length": asset.length,
|
|
1090
1629
|
"Cache-Control": "no-cache"
|
|
1091
1630
|
});
|
|
1092
1631
|
asset.content.pipe(res);
|
|
1093
1632
|
}
|
|
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
1633
|
async function handleNodeUIPath(req, res, urlPath) {
|
|
1111
|
-
|
|
1634
|
+
const verdict = guard(req);
|
|
1635
|
+
if (!verdict.ok) {
|
|
1636
|
+
sendJson(res, verdict.status, {
|
|
1637
|
+
ok: false,
|
|
1638
|
+
error: { code: verdict.code, message: verdict.message }
|
|
1639
|
+
});
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
if (verdict.setToken !== void 0) {
|
|
1643
|
+
res.writeHead(302, {
|
|
1644
|
+
...SECURITY_HEADERS,
|
|
1645
|
+
"Set-Cookie": `${AUTH_COOKIE}=${encodeURIComponent(verdict.setToken)}; Path=${path}; HttpOnly; SameSite=Strict`,
|
|
1646
|
+
Location: `${path}/`,
|
|
1647
|
+
"Cache-Control": "no-store"
|
|
1648
|
+
});
|
|
1649
|
+
res.end();
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1112
1652
|
const apiBase = `${path}/api`;
|
|
1113
1653
|
if (urlPath === apiBase || urlPath.startsWith(`${apiBase}/`)) {
|
|
1114
1654
|
await handleApi(req, res, urlPath.slice(apiBase.length));
|
|
@@ -1192,6 +1732,9 @@ function createNodeUI(options = {}) {
|
|
|
1192
1732
|
}
|
|
1193
1733
|
notFound(res);
|
|
1194
1734
|
},
|
|
1735
|
+
setRoutes(source) {
|
|
1736
|
+
ctx.store["routes-source"] = source;
|
|
1737
|
+
},
|
|
1195
1738
|
mark(name) {
|
|
1196
1739
|
startupTracker.mark(name);
|
|
1197
1740
|
},
|
|
@@ -1209,6 +1752,7 @@ function createNodeUI(options = {}) {
|
|
|
1209
1752
|
}
|
|
1210
1753
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1211
1754
|
0 && (module.exports = {
|
|
1755
|
+
AUTH_COOKIE,
|
|
1212
1756
|
ConfirmationStore,
|
|
1213
1757
|
CpuProvider,
|
|
1214
1758
|
DEFAULT_LOG_SIZE,
|
|
@@ -1219,6 +1763,7 @@ function createNodeUI(options = {}) {
|
|
|
1219
1763
|
LogsProvider,
|
|
1220
1764
|
MemoryProvider,
|
|
1221
1765
|
MetricsProvider,
|
|
1766
|
+
OutgoingProvider,
|
|
1222
1767
|
ProviderRegistry,
|
|
1223
1768
|
RequestsProvider,
|
|
1224
1769
|
RingBuffer,
|
|
@@ -1227,11 +1772,17 @@ function createNodeUI(options = {}) {
|
|
|
1227
1772
|
SECRET_MASKED,
|
|
1228
1773
|
Sampler,
|
|
1229
1774
|
StartupTracker,
|
|
1775
|
+
createGuard,
|
|
1230
1776
|
createNodeUI,
|
|
1231
1777
|
extractRoutes,
|
|
1778
|
+
hostnameFromHostHeader,
|
|
1232
1779
|
interceptConsole,
|
|
1780
|
+
interceptOutgoing,
|
|
1233
1781
|
isLoopbackAddress,
|
|
1782
|
+
isLoopbackHostname,
|
|
1783
|
+
maskSecretText,
|
|
1234
1784
|
maskSecrets,
|
|
1785
|
+
matchesAddress,
|
|
1235
1786
|
resolveActivation,
|
|
1236
1787
|
resolveStaticAsset,
|
|
1237
1788
|
serializeEnvelope,
|