@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.mjs
CHANGED
|
@@ -50,7 +50,15 @@ var RingBuffer = class {
|
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
// src/safety.ts
|
|
53
|
-
var SECRET_KEY_PATTERN = /token|key|secret|
|
|
53
|
+
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;
|
|
54
|
+
var TEXT_PAIR_PATTERN = /\b([\w.-]*(?:token|secret|passw(?:or)?d|api[_-]?key|credential|authorization|cookie|dsn)[\w.-]*["']?)(\s*[=:]\s*)("[^"]*"|'[^']*'|[^\s,;&"'}]+)/gi;
|
|
55
|
+
var URL_CREDENTIALS_PATTERN = /([a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:)[^\s@/]+(@)/gi;
|
|
56
|
+
var BEARER_PATTERN = /\b(Bearer|Basic)\s+[A-Za-z0-9\-._~+/]+=*/gi;
|
|
57
|
+
var JWT_PATTERN = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]*/g;
|
|
58
|
+
function maskSecretText(text) {
|
|
59
|
+
if (text.length < 6) return text;
|
|
60
|
+
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}`);
|
|
61
|
+
}
|
|
54
62
|
function resolveActivation(env) {
|
|
55
63
|
if (env.NODEUI_ENABLED === "true") {
|
|
56
64
|
return { active: true, reason: "activated by NODEUI_ENABLED=true" };
|
|
@@ -73,7 +81,55 @@ function isLoopbackAddress(address) {
|
|
|
73
81
|
if (!address) return false;
|
|
74
82
|
return address.startsWith("127.") || address === "::1" || address.startsWith("::ffff:127.") || address === "localhost";
|
|
75
83
|
}
|
|
84
|
+
function normalizeIp(address) {
|
|
85
|
+
return address.startsWith("::ffff:") ? address.slice(7) : address;
|
|
86
|
+
}
|
|
87
|
+
function ipv4ToInt(ip) {
|
|
88
|
+
const parts = ip.split(".");
|
|
89
|
+
if (parts.length !== 4) return null;
|
|
90
|
+
let out = 0;
|
|
91
|
+
for (const part of parts) {
|
|
92
|
+
const n = Number(part);
|
|
93
|
+
if (!Number.isInteger(n) || n < 0 || n > 255) return null;
|
|
94
|
+
out = out * 256 + n;
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
function matchesAddress(address, allowed) {
|
|
99
|
+
if (!address) return false;
|
|
100
|
+
const ip = normalizeIp(address);
|
|
101
|
+
for (const entry of allowed) {
|
|
102
|
+
if (!entry.includes("/")) {
|
|
103
|
+
if (normalizeIp(entry) === ip) return true;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const [base, bitsRaw] = entry.split("/");
|
|
107
|
+
const bits = Number(bitsRaw);
|
|
108
|
+
const a = ipv4ToInt(ip);
|
|
109
|
+
const b = ipv4ToInt(base ?? "");
|
|
110
|
+
if (a === null || b === null || !Number.isInteger(bits) || bits < 0 || bits > 32) continue;
|
|
111
|
+
const mask = bits === 0 ? 0 : 4294967295 << 32 - bits >>> 0;
|
|
112
|
+
if ((a & mask) >>> 0 === (b & mask) >>> 0) return true;
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
function hostnameFromHostHeader(value) {
|
|
117
|
+
if (!value) return null;
|
|
118
|
+
const trimmed = value.trim().toLowerCase();
|
|
119
|
+
if (trimmed.startsWith("[")) {
|
|
120
|
+
const end = trimmed.indexOf("]");
|
|
121
|
+
return end === -1 ? null : trimmed.slice(1, end);
|
|
122
|
+
}
|
|
123
|
+
return trimmed.split(":")[0] || null;
|
|
124
|
+
}
|
|
125
|
+
function isLoopbackHostname(hostname) {
|
|
126
|
+
if (!hostname) return false;
|
|
127
|
+
return hostname === "localhost" || hostname.endsWith(".localhost") || isLoopbackAddress(hostname);
|
|
128
|
+
}
|
|
76
129
|
function maskSecrets(value, pattern = SECRET_KEY_PATTERN) {
|
|
130
|
+
if (typeof value === "string") {
|
|
131
|
+
return maskSecretText(value);
|
|
132
|
+
}
|
|
77
133
|
if (value !== null && typeof value === "object") {
|
|
78
134
|
if (Array.isArray(value)) {
|
|
79
135
|
return value.map((v) => maskSecrets(v, pattern));
|
|
@@ -97,16 +153,132 @@ function maskSecrets(value, pattern = SECRET_KEY_PATTERN) {
|
|
|
97
153
|
return value;
|
|
98
154
|
}
|
|
99
155
|
|
|
156
|
+
// src/guard.ts
|
|
157
|
+
import { createHash, timingSafeEqual } from "crypto";
|
|
158
|
+
var AUTH_COOKIE = "nodeui_token";
|
|
159
|
+
var FORWARD_HEADERS = ["x-forwarded-for", "x-forwarded-host", "forwarded", "x-real-ip"];
|
|
160
|
+
function digest(value) {
|
|
161
|
+
return createHash("sha256").update(value).digest();
|
|
162
|
+
}
|
|
163
|
+
function tokenEquals(a, b) {
|
|
164
|
+
return timingSafeEqual(digest(a), digest(b));
|
|
165
|
+
}
|
|
166
|
+
function readCookie(header, name) {
|
|
167
|
+
if (!header) return void 0;
|
|
168
|
+
for (const part of header.split(";")) {
|
|
169
|
+
const [k, ...rest] = part.trim().split("=");
|
|
170
|
+
if (k === name) {
|
|
171
|
+
try {
|
|
172
|
+
return decodeURIComponent(rest.join("="));
|
|
173
|
+
} catch {
|
|
174
|
+
return void 0;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return void 0;
|
|
179
|
+
}
|
|
180
|
+
function firstHeader(value) {
|
|
181
|
+
return Array.isArray(value) ? value[0] : value;
|
|
182
|
+
}
|
|
183
|
+
function deny(status, code, message) {
|
|
184
|
+
return { ok: false, status, code, message };
|
|
185
|
+
}
|
|
186
|
+
function createGuard(options) {
|
|
187
|
+
const strict = isLoopbackAddress(options.host);
|
|
188
|
+
const allowedHosts = options.allowedHosts.map((h) => h.toLowerCase());
|
|
189
|
+
const allowedOrigins = options.allowedOrigins.map((o) => o.toLowerCase().replace(/\/+$/, ""));
|
|
190
|
+
return (req) => {
|
|
191
|
+
const remote = req.socket.remoteAddress;
|
|
192
|
+
const remoteLoopback = isLoopbackAddress(remote);
|
|
193
|
+
const remoteAllowed = remoteLoopback || matchesAddress(remote, options.allowedRemoteAddresses);
|
|
194
|
+
if (strict && !remoteAllowed) {
|
|
195
|
+
return deny(
|
|
196
|
+
403,
|
|
197
|
+
"forbidden",
|
|
198
|
+
"nodeui is loopback-only; requests from other hosts are rejected"
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
if (!strict && options.allowedRemoteAddresses.length > 0 && !remoteAllowed) {
|
|
202
|
+
return deny(403, "forbidden", "remote address is not in the nodeui allow-list");
|
|
203
|
+
}
|
|
204
|
+
if (strict && !options.trustProxy && !matchesAddress(remote, options.allowedRemoteAddresses)) {
|
|
205
|
+
if (FORWARD_HEADERS.some((h) => req.headers[h] !== void 0)) {
|
|
206
|
+
return deny(
|
|
207
|
+
403,
|
|
208
|
+
"forbidden",
|
|
209
|
+
"proxied requests are rejected (set trustProxy to allow forwarded traffic)"
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const hostHeader = firstHeader(req.headers.host);
|
|
214
|
+
const hostname = hostnameFromHostHeader(hostHeader);
|
|
215
|
+
if (hostname !== null) {
|
|
216
|
+
const hostOk = isLoopbackHostname(hostname) || allowedHosts.includes(hostname) || !strict && allowedHosts.length === 0;
|
|
217
|
+
if (!hostOk) {
|
|
218
|
+
return deny(
|
|
219
|
+
403,
|
|
220
|
+
"forbidden",
|
|
221
|
+
`Host "${hostname}" is not allowed (add it to allowedHosts to use nodeui via this name)`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const origin = firstHeader(req.headers.origin);
|
|
226
|
+
if (origin !== void 0) {
|
|
227
|
+
let originOk = false;
|
|
228
|
+
const normalized = origin.toLowerCase().replace(/\/+$/, "");
|
|
229
|
+
if (allowedOrigins.includes(normalized)) {
|
|
230
|
+
originOk = true;
|
|
231
|
+
} else {
|
|
232
|
+
try {
|
|
233
|
+
originOk = hostHeader !== void 0 && new URL(origin).host === hostHeader.toLowerCase();
|
|
234
|
+
} catch {
|
|
235
|
+
originOk = false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (!originOk) {
|
|
239
|
+
return deny(403, "forbidden", "cross-origin requests to nodeui are rejected");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (options.authToken) {
|
|
243
|
+
const authHeader = firstHeader(req.headers.authorization);
|
|
244
|
+
const bearer = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : void 0;
|
|
245
|
+
const queryToken = new URL(req.url ?? "/", "http://localhost").searchParams.get("token");
|
|
246
|
+
const cookieToken = readCookie(firstHeader(req.headers.cookie), AUTH_COOKIE);
|
|
247
|
+
const candidates = [
|
|
248
|
+
bearer,
|
|
249
|
+
firstHeader(req.headers["x-nodeui-token"]),
|
|
250
|
+
cookieToken,
|
|
251
|
+
queryToken ?? void 0
|
|
252
|
+
];
|
|
253
|
+
const matched = candidates.find((c) => c !== void 0 && tokenEquals(c, options.authToken));
|
|
254
|
+
if (matched === void 0) {
|
|
255
|
+
return deny(401, "unauthorized", "a valid nodeui token is required");
|
|
256
|
+
}
|
|
257
|
+
if (queryToken !== null && matched === queryToken) {
|
|
258
|
+
return { ok: true, setToken: queryToken };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return { ok: true };
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
100
265
|
// src/confirmations.ts
|
|
101
266
|
import { randomBytes } from "crypto";
|
|
102
267
|
var ConfirmationStore = class {
|
|
103
|
-
constructor(ttlMs) {
|
|
268
|
+
constructor(ttlMs, maxOutstanding = 100) {
|
|
104
269
|
this.ttlMs = ttlMs;
|
|
270
|
+
this.maxOutstanding = maxOutstanding;
|
|
105
271
|
}
|
|
106
272
|
ttlMs;
|
|
273
|
+
maxOutstanding;
|
|
107
274
|
nonces = /* @__PURE__ */ new Map();
|
|
108
275
|
issue() {
|
|
109
276
|
this.prune();
|
|
277
|
+
while (this.nonces.size >= this.maxOutstanding) {
|
|
278
|
+
const oldest = this.nonces.keys().next().value;
|
|
279
|
+
if (oldest === void 0) break;
|
|
280
|
+
this.nonces.delete(oldest);
|
|
281
|
+
}
|
|
110
282
|
const nonce = randomBytes(16).toString("hex");
|
|
111
283
|
const expiresAtMs = Date.now() + this.ttlMs;
|
|
112
284
|
this.nonces.set(nonce, expiresAtMs);
|
|
@@ -419,15 +591,65 @@ var EventLoopLagProvider = class {
|
|
|
419
591
|
// src/providers/health.ts
|
|
420
592
|
var LAG_OK_MS = 50;
|
|
421
593
|
var LAG_DEGRADED_MS = 200;
|
|
594
|
+
var CHECK_TIMEOUT_MS = 3e3;
|
|
595
|
+
var CHECK_CACHE_MS = 5e3;
|
|
596
|
+
function withTimeout(promise, ms) {
|
|
597
|
+
return new Promise((resolve3, reject) => {
|
|
598
|
+
const timer = setTimeout(() => reject(new Error(`timed out after ${ms}ms`)), ms);
|
|
599
|
+
promise.then(
|
|
600
|
+
(value) => {
|
|
601
|
+
clearTimeout(timer);
|
|
602
|
+
resolve3(value);
|
|
603
|
+
},
|
|
604
|
+
(err) => {
|
|
605
|
+
clearTimeout(timer);
|
|
606
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
607
|
+
}
|
|
608
|
+
);
|
|
609
|
+
});
|
|
610
|
+
}
|
|
422
611
|
var HealthProvider = class {
|
|
612
|
+
constructor(checks = {}) {
|
|
613
|
+
this.checks = checks;
|
|
614
|
+
}
|
|
615
|
+
checks;
|
|
423
616
|
id = "health";
|
|
424
|
-
|
|
617
|
+
cache = null;
|
|
618
|
+
async runChecks() {
|
|
619
|
+
const names = Object.keys(this.checks);
|
|
620
|
+
if (names.length === 0) return [];
|
|
621
|
+
if (this.cache && Date.now() - this.cache.at < CHECK_CACHE_MS) return this.cache.results;
|
|
622
|
+
const results = await Promise.all(
|
|
623
|
+
names.map(async (name) => {
|
|
624
|
+
const started = Date.now();
|
|
625
|
+
try {
|
|
626
|
+
const outcome = await withTimeout(
|
|
627
|
+
Promise.resolve().then(() => this.checks[name]()),
|
|
628
|
+
CHECK_TIMEOUT_MS
|
|
629
|
+
);
|
|
630
|
+
if (outcome === false) throw new Error("check returned false");
|
|
631
|
+
return { name, status: "up", durationMs: Date.now() - started };
|
|
632
|
+
} catch (err) {
|
|
633
|
+
return {
|
|
634
|
+
name,
|
|
635
|
+
status: "down",
|
|
636
|
+
durationMs: Date.now() - started,
|
|
637
|
+
error: err instanceof Error ? err.message : String(err)
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
})
|
|
641
|
+
);
|
|
642
|
+
this.cache = { at: Date.now(), results };
|
|
643
|
+
return results;
|
|
644
|
+
}
|
|
645
|
+
async get(ctx) {
|
|
425
646
|
const loop = ctx.store["event-loop"];
|
|
426
647
|
const memory = ctx.store.memory;
|
|
427
648
|
const lagMs = typeof loop?.currentMs === "number" ? loop.currentMs : null;
|
|
428
649
|
const totalMem = typeof memory?.totalMem === "number" ? memory.totalMem : 0;
|
|
429
650
|
const rss = typeof memory?.rss === "number" ? memory.rss : 0;
|
|
430
651
|
const memoryUsedPercent = totalMem > 0 ? rss / totalMem * 100 : null;
|
|
652
|
+
const checks = await this.runChecks();
|
|
431
653
|
let status;
|
|
432
654
|
let statusReason;
|
|
433
655
|
if (lagMs === null) {
|
|
@@ -443,6 +665,11 @@ var HealthProvider = class {
|
|
|
443
665
|
status = "critical";
|
|
444
666
|
statusReason = `event-loop lag ${lagMs.toFixed(1)}ms above ${LAG_DEGRADED_MS}ms`;
|
|
445
667
|
}
|
|
668
|
+
const down = checks.filter((c) => c.status === "down");
|
|
669
|
+
if (down.length > 0) {
|
|
670
|
+
status = "critical";
|
|
671
|
+
statusReason = `dependency down: ${down.map((c) => c.name).join(", ")}`;
|
|
672
|
+
}
|
|
446
673
|
return {
|
|
447
674
|
ok: true,
|
|
448
675
|
data: {
|
|
@@ -453,19 +680,202 @@ var HealthProvider = class {
|
|
|
453
680
|
nodeVersion: process.version,
|
|
454
681
|
platform: process.platform,
|
|
455
682
|
eventLoopLagMs: lagMs,
|
|
456
|
-
memoryUsedPercent
|
|
683
|
+
memoryUsedPercent,
|
|
684
|
+
checks
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
// src/providers/outgoing.ts
|
|
691
|
+
import diagnosticsChannel from "diagnostics_channel";
|
|
692
|
+
import http from "http";
|
|
693
|
+
import https from "https";
|
|
694
|
+
import { syncBuiltinESMExports } from "module";
|
|
695
|
+
var RESPONSE_PAGE_SIZE = 100;
|
|
696
|
+
var recorders = /* @__PURE__ */ new Set();
|
|
697
|
+
var originals = /* @__PURE__ */ new Map();
|
|
698
|
+
var undiciUnsubscribe = null;
|
|
699
|
+
function describeTarget(defaultProtocol, args) {
|
|
700
|
+
try {
|
|
701
|
+
let method = "GET";
|
|
702
|
+
let host = "unknown";
|
|
703
|
+
let path2 = "/";
|
|
704
|
+
const [first, second] = args;
|
|
705
|
+
const opts = typeof first === "string" || first instanceof URL ? second : first;
|
|
706
|
+
if (typeof first === "string" || first instanceof URL) {
|
|
707
|
+
const parsed = new URL(first.toString());
|
|
708
|
+
host = parsed.host;
|
|
709
|
+
path2 = parsed.pathname;
|
|
710
|
+
}
|
|
711
|
+
if (opts && typeof opts === "object") {
|
|
712
|
+
if (typeof opts.method === "string") method = opts.method.toUpperCase();
|
|
713
|
+
if (typeof first !== "string" && !(first instanceof URL)) {
|
|
714
|
+
const name = opts.hostname ?? opts.host ?? "localhost";
|
|
715
|
+
host = opts.port ? `${name}:${String(opts.port)}` : name;
|
|
716
|
+
path2 = typeof opts.path === "string" ? opts.path.split("?")[0] ?? "/" : "/";
|
|
717
|
+
const protocol = typeof opts.protocol === "string" ? opts.protocol : defaultProtocol;
|
|
718
|
+
return { method, url: `${protocol}//${host}${path2}` };
|
|
457
719
|
}
|
|
720
|
+
}
|
|
721
|
+
return { method, url: `${defaultProtocol}//${host}${path2}` };
|
|
722
|
+
} catch {
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
function emit(entry) {
|
|
727
|
+
for (const record of recorders) {
|
|
728
|
+
try {
|
|
729
|
+
record(entry);
|
|
730
|
+
} catch {
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
function wrapModule(mod, protocol) {
|
|
735
|
+
for (const name of ["request", "get"]) {
|
|
736
|
+
const key = `${protocol}${name}`;
|
|
737
|
+
const original = mod[name];
|
|
738
|
+
originals.set(key, original);
|
|
739
|
+
const wrapped = function(...args) {
|
|
740
|
+
const req = original.apply(this, args);
|
|
741
|
+
const target = recorders.size > 0 ? describeTarget(protocol, args) : null;
|
|
742
|
+
if (target && req && typeof req.once === "function") {
|
|
743
|
+
const started = process.hrtime.bigint();
|
|
744
|
+
const timestampMs = Date.now();
|
|
745
|
+
let done = false;
|
|
746
|
+
const finish = (status, error) => {
|
|
747
|
+
if (done) return;
|
|
748
|
+
done = true;
|
|
749
|
+
emit({
|
|
750
|
+
...target,
|
|
751
|
+
status,
|
|
752
|
+
durationMs: Number(process.hrtime.bigint() - started) / 1e6,
|
|
753
|
+
timestampMs,
|
|
754
|
+
...error ? { error } : {}
|
|
755
|
+
});
|
|
756
|
+
};
|
|
757
|
+
req.once("response", (res) => {
|
|
758
|
+
const status = res.statusCode ?? null;
|
|
759
|
+
res.once("end", () => finish(status));
|
|
760
|
+
res.once("close", () => finish(status));
|
|
761
|
+
});
|
|
762
|
+
req.once("error", (err) => finish(null, err.message));
|
|
763
|
+
}
|
|
764
|
+
return req;
|
|
458
765
|
};
|
|
766
|
+
mod[name] = wrapped;
|
|
767
|
+
}
|
|
768
|
+
syncBuiltinESMExports();
|
|
769
|
+
}
|
|
770
|
+
function unwrapModule(mod, protocol) {
|
|
771
|
+
for (const name of ["request", "get"]) {
|
|
772
|
+
const original = originals.get(`${protocol}${name}`);
|
|
773
|
+
if (original) mod[name] = original;
|
|
774
|
+
originals.delete(`${protocol}${name}`);
|
|
775
|
+
}
|
|
776
|
+
syncBuiltinESMExports();
|
|
777
|
+
}
|
|
778
|
+
function subscribeUndici() {
|
|
779
|
+
const started = /* @__PURE__ */ new WeakMap();
|
|
780
|
+
const statuses = /* @__PURE__ */ new WeakMap();
|
|
781
|
+
const target = (req) => ({
|
|
782
|
+
method: (req.method ?? "GET").toUpperCase(),
|
|
783
|
+
url: `${String(req.origin ?? "")}${(req.path ?? "/").split("?")[0]}`
|
|
784
|
+
});
|
|
785
|
+
const onCreate = (message) => {
|
|
786
|
+
const request = message.request;
|
|
787
|
+
if (request) started.set(request, { at: process.hrtime.bigint(), ts: Date.now() });
|
|
788
|
+
};
|
|
789
|
+
const onHeaders = (message) => {
|
|
790
|
+
const { request, response } = message;
|
|
791
|
+
if (request && typeof response?.statusCode === "number") {
|
|
792
|
+
statuses.set(request, response.statusCode);
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
const finish = (request, error) => {
|
|
796
|
+
if (!request) return;
|
|
797
|
+
const begin = started.get(request);
|
|
798
|
+
if (!begin) return;
|
|
799
|
+
started.delete(request);
|
|
800
|
+
emit({
|
|
801
|
+
...target(request),
|
|
802
|
+
status: statuses.get(request) ?? null,
|
|
803
|
+
durationMs: Number(process.hrtime.bigint() - begin.at) / 1e6,
|
|
804
|
+
timestampMs: begin.ts,
|
|
805
|
+
...error ? { error } : {}
|
|
806
|
+
});
|
|
807
|
+
};
|
|
808
|
+
const onTrailers = (message) => finish(message.request);
|
|
809
|
+
const onError = (message) => {
|
|
810
|
+
const { request, error } = message;
|
|
811
|
+
finish(request, error?.message ?? "request failed");
|
|
812
|
+
};
|
|
813
|
+
diagnosticsChannel.subscribe("undici:request:create", onCreate);
|
|
814
|
+
diagnosticsChannel.subscribe("undici:request:headers", onHeaders);
|
|
815
|
+
diagnosticsChannel.subscribe("undici:request:trailers", onTrailers);
|
|
816
|
+
diagnosticsChannel.subscribe("undici:request:error", onError);
|
|
817
|
+
return () => {
|
|
818
|
+
diagnosticsChannel.unsubscribe("undici:request:create", onCreate);
|
|
819
|
+
diagnosticsChannel.unsubscribe("undici:request:headers", onHeaders);
|
|
820
|
+
diagnosticsChannel.unsubscribe("undici:request:trailers", onTrailers);
|
|
821
|
+
diagnosticsChannel.unsubscribe("undici:request:error", onError);
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
function interceptOutgoing(record) {
|
|
825
|
+
const first = recorders.size === 0;
|
|
826
|
+
recorders.add(record);
|
|
827
|
+
if (first) {
|
|
828
|
+
wrapModule(http, "http:");
|
|
829
|
+
wrapModule(https, "https:");
|
|
830
|
+
try {
|
|
831
|
+
undiciUnsubscribe = subscribeUndici();
|
|
832
|
+
} catch {
|
|
833
|
+
undiciUnsubscribe = null;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return () => {
|
|
837
|
+
if (!recorders.delete(record) || recorders.size > 0) return;
|
|
838
|
+
unwrapModule(http, "http:");
|
|
839
|
+
unwrapModule(https, "https:");
|
|
840
|
+
undiciUnsubscribe?.();
|
|
841
|
+
undiciUnsubscribe = null;
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
var OutgoingProvider = class {
|
|
845
|
+
id = "outgoing";
|
|
846
|
+
buffer;
|
|
847
|
+
nextId = 1;
|
|
848
|
+
failed = 0;
|
|
849
|
+
release = null;
|
|
850
|
+
constructor(size) {
|
|
851
|
+
this.buffer = new RingBuffer(size);
|
|
852
|
+
}
|
|
853
|
+
start() {
|
|
854
|
+
if (this.release) return;
|
|
855
|
+
this.release = interceptOutgoing((entry) => this.record(entry));
|
|
856
|
+
}
|
|
857
|
+
stop() {
|
|
858
|
+
this.release?.();
|
|
859
|
+
this.release = null;
|
|
860
|
+
}
|
|
861
|
+
record(entry) {
|
|
862
|
+
this.buffer.push({ id: this.nextId, ...entry });
|
|
863
|
+
this.nextId += 1;
|
|
864
|
+
if (entry.error || entry.status !== null && entry.status >= 500) this.failed += 1;
|
|
865
|
+
}
|
|
866
|
+
get() {
|
|
867
|
+
const entries = this.buffer.slice(Math.max(0, this.buffer.length - RESPONSE_PAGE_SIZE));
|
|
868
|
+
return { ok: true, data: { total: this.nextId - 1, failed: this.failed, entries } };
|
|
459
869
|
}
|
|
460
870
|
};
|
|
461
871
|
|
|
462
872
|
// src/providers/heap-snapshot.ts
|
|
463
|
-
import { mkdir, stat } from "fs/promises";
|
|
873
|
+
import { chmod, mkdir, stat } from "fs/promises";
|
|
464
874
|
import { tmpdir } from "os";
|
|
465
875
|
import { join as join2 } from "path";
|
|
466
876
|
import { writeHeapSnapshot } from "v8";
|
|
467
877
|
function defaultSnapshotDir() {
|
|
468
|
-
return process.env.NODEUI_HEAP_SNAPSHOT_DIR ?? tmpdir();
|
|
878
|
+
return process.env.NODEUI_HEAP_SNAPSHOT_DIR ?? join2(tmpdir(), "nodeui-heap");
|
|
469
879
|
}
|
|
470
880
|
var HeapSnapshotProvider = class {
|
|
471
881
|
id = "heap-snapshot";
|
|
@@ -479,8 +889,9 @@ var HeapSnapshotProvider = class {
|
|
|
479
889
|
const fileName = `nodeui-heap-${process.pid}-${stamp}.heapsnapshot`;
|
|
480
890
|
const filePath = join2(dir, fileName);
|
|
481
891
|
try {
|
|
482
|
-
await mkdir(dir, { recursive: true });
|
|
892
|
+
await mkdir(dir, { recursive: true, mode: 448 });
|
|
483
893
|
writeHeapSnapshot(filePath);
|
|
894
|
+
await chmod(filePath, 384).catch(() => void 0);
|
|
484
895
|
const fileStat = await stat(filePath);
|
|
485
896
|
const data = {
|
|
486
897
|
fileName,
|
|
@@ -503,7 +914,7 @@ var HeapSnapshotProvider = class {
|
|
|
503
914
|
};
|
|
504
915
|
|
|
505
916
|
// src/providers/requests.ts
|
|
506
|
-
var
|
|
917
|
+
var RESPONSE_PAGE_SIZE2 = 100;
|
|
507
918
|
var RequestsProvider = class {
|
|
508
919
|
id = "requests";
|
|
509
920
|
buffer;
|
|
@@ -519,7 +930,7 @@ var RequestsProvider = class {
|
|
|
519
930
|
return this.buffer.length;
|
|
520
931
|
}
|
|
521
932
|
get() {
|
|
522
|
-
const entries = this.buffer.slice(Math.max(0, this.buffer.length -
|
|
933
|
+
const entries = this.buffer.slice(Math.max(0, this.buffer.length - RESPONSE_PAGE_SIZE2));
|
|
523
934
|
return { ok: true, data: { total: this.buffer.length, entries } };
|
|
524
935
|
}
|
|
525
936
|
};
|
|
@@ -634,6 +1045,11 @@ var RoutesProvider = class {
|
|
|
634
1045
|
id = "routes";
|
|
635
1046
|
cache = /* @__PURE__ */ new WeakMap();
|
|
636
1047
|
get(ctx) {
|
|
1048
|
+
const source = ctx.store["routes-source"];
|
|
1049
|
+
if (source !== void 0) {
|
|
1050
|
+
const routes2 = typeof source === "function" ? source() : source;
|
|
1051
|
+
return { ok: true, data: { routes: routes2 } };
|
|
1052
|
+
}
|
|
637
1053
|
const router = ctx.store["express-router"];
|
|
638
1054
|
if (!router) {
|
|
639
1055
|
return {
|
|
@@ -682,14 +1098,14 @@ function formatArg(arg) {
|
|
|
682
1098
|
var refs = 0;
|
|
683
1099
|
var pushes = /* @__PURE__ */ new Set();
|
|
684
1100
|
var wrappers = /* @__PURE__ */ new Map();
|
|
685
|
-
var
|
|
1101
|
+
var originals2 = {};
|
|
686
1102
|
function consoleRef() {
|
|
687
1103
|
return console;
|
|
688
1104
|
}
|
|
689
1105
|
function installWrappers() {
|
|
690
1106
|
for (const [method, level] of CONSOLE_METHODS) {
|
|
691
1107
|
const original = consoleRef()[method];
|
|
692
|
-
|
|
1108
|
+
originals2[method] = original;
|
|
693
1109
|
const wrapper = (...args) => {
|
|
694
1110
|
for (const push of pushes) {
|
|
695
1111
|
try {
|
|
@@ -707,7 +1123,7 @@ function uninstallWrappers() {
|
|
|
707
1123
|
for (const [method] of CONSOLE_METHODS) {
|
|
708
1124
|
const current = consoleRef()[method];
|
|
709
1125
|
if (current === wrappers.get(method)) {
|
|
710
|
-
const original =
|
|
1126
|
+
const original = originals2[method];
|
|
711
1127
|
if (original) {
|
|
712
1128
|
consoleRef()[method] = original;
|
|
713
1129
|
} else {
|
|
@@ -716,7 +1132,7 @@ function uninstallWrappers() {
|
|
|
716
1132
|
}
|
|
717
1133
|
}
|
|
718
1134
|
wrappers.clear();
|
|
719
|
-
|
|
1135
|
+
originals2 = {};
|
|
720
1136
|
}
|
|
721
1137
|
function interceptConsole(push) {
|
|
722
1138
|
refs += 1;
|
|
@@ -763,9 +1179,10 @@ var LogsProvider = class {
|
|
|
763
1179
|
};
|
|
764
1180
|
|
|
765
1181
|
// src/sse.ts
|
|
766
|
-
function startSse(res) {
|
|
1182
|
+
function startSse(res, extraHeaders = {}) {
|
|
767
1183
|
let closed = false;
|
|
768
1184
|
res.writeHead(200, {
|
|
1185
|
+
...extraHeaders,
|
|
769
1186
|
"Content-Type": "text/event-stream",
|
|
770
1187
|
"Cache-Control": "no-cache",
|
|
771
1188
|
Connection: "keep-alive",
|
|
@@ -793,11 +1210,38 @@ function startSse(res) {
|
|
|
793
1210
|
|
|
794
1211
|
// src/server.ts
|
|
795
1212
|
import { tmpdir as tmpdir2 } from "os";
|
|
796
|
-
import { resolve as resolve2 } from "path";
|
|
1213
|
+
import { join as join3, resolve as resolve2 } from "path";
|
|
797
1214
|
var STATIC_ROOT = resolve2(__dirname, "..", "static");
|
|
798
1215
|
var SSE_HEARTBEAT_MS = 15e3;
|
|
799
|
-
function serializeEnvelope(envelope) {
|
|
800
|
-
return JSON.stringify(maskSecrets(envelope));
|
|
1216
|
+
function serializeEnvelope(envelope, mask = true) {
|
|
1217
|
+
return JSON.stringify(mask ? maskSecrets(envelope) : envelope);
|
|
1218
|
+
}
|
|
1219
|
+
var BUILT_IN_PANELS = /* @__PURE__ */ new Set([
|
|
1220
|
+
"health",
|
|
1221
|
+
"memory",
|
|
1222
|
+
"cpu",
|
|
1223
|
+
"event-loop",
|
|
1224
|
+
"heap-snapshot",
|
|
1225
|
+
"startup",
|
|
1226
|
+
"requests",
|
|
1227
|
+
"env",
|
|
1228
|
+
"routes",
|
|
1229
|
+
"logs",
|
|
1230
|
+
"metrics",
|
|
1231
|
+
"outgoing"
|
|
1232
|
+
]);
|
|
1233
|
+
var PLUGIN_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
1234
|
+
var RESERVED_API_NAMES = /* @__PURE__ */ new Set(["config", "live", "confirmations"]);
|
|
1235
|
+
var SECURITY_HEADERS = {
|
|
1236
|
+
"X-Content-Type-Options": "nosniff",
|
|
1237
|
+
"X-Frame-Options": "DENY",
|
|
1238
|
+
"Referrer-Policy": "no-referrer",
|
|
1239
|
+
"Cross-Origin-Resource-Policy": "same-origin"
|
|
1240
|
+
};
|
|
1241
|
+
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'";
|
|
1242
|
+
function listFrom(value, raw) {
|
|
1243
|
+
if (value) return value;
|
|
1244
|
+
return (raw ?? "").split(",").map((v) => v.trim()).filter(Boolean);
|
|
801
1245
|
}
|
|
802
1246
|
function pathnameOf(req) {
|
|
803
1247
|
const raw = req.originalUrl ?? req.url ?? "/";
|
|
@@ -857,19 +1301,46 @@ function createNodeUI(options = {}) {
|
|
|
857
1301
|
env.NODEUI_CONFIRM_TTL_MS ? Number(env.NODEUI_CONFIRM_TTL_MS) : 6e4,
|
|
858
1302
|
"NODEUI_CONFIRM_TTL_MS"
|
|
859
1303
|
),
|
|
860
|
-
heapSnapshotDir: options.heapSnapshotDir ?? env.NODEUI_HEAP_SNAPSHOT_DIR ?? tmpdir2()
|
|
1304
|
+
heapSnapshotDir: options.heapSnapshotDir ?? env.NODEUI_HEAP_SNAPSHOT_DIR ?? join3(tmpdir2(), "nodeui-heap"),
|
|
1305
|
+
allowedHosts: listFrom(options.allowedHosts, env.NODEUI_ALLOWED_HOSTS),
|
|
1306
|
+
allowedOrigins: listFrom(options.allowedOrigins, env.NODEUI_ALLOWED_ORIGINS),
|
|
1307
|
+
allowedRemoteAddresses: listFrom(options.allowedRemoteAddresses, env.NODEUI_ALLOWED_REMOTE),
|
|
1308
|
+
trustProxy: options.trustProxy ?? env.NODEUI_TRUST_PROXY === "true",
|
|
1309
|
+
authRequired: Boolean(options.authToken ?? env.NODEUI_TOKEN),
|
|
1310
|
+
maxSseClients: positiveInt(
|
|
1311
|
+
options.maxSseClients,
|
|
1312
|
+
env.NODEUI_MAX_SSE_CLIENTS ? Number(env.NODEUI_MAX_SSE_CLIENTS) : 10,
|
|
1313
|
+
"NODEUI_MAX_SSE_CLIENTS"
|
|
1314
|
+
)
|
|
861
1315
|
};
|
|
862
|
-
|
|
1316
|
+
const authToken = options.authToken ?? env.NODEUI_TOKEN;
|
|
1317
|
+
const guard = createGuard({
|
|
1318
|
+
host: config.host,
|
|
1319
|
+
allowedHosts: config.allowedHosts,
|
|
1320
|
+
allowedOrigins: config.allowedOrigins,
|
|
1321
|
+
allowedRemoteAddresses: config.allowedRemoteAddresses,
|
|
1322
|
+
trustProxy: config.trustProxy,
|
|
1323
|
+
authToken: authToken || void 0
|
|
1324
|
+
});
|
|
1325
|
+
if (config.enabled && !isLoopbackAddress(config.host)) {
|
|
1326
|
+
console.warn(
|
|
1327
|
+
`[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.")
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
if (config.enabled && env.NODE_ENV === "production") {
|
|
863
1331
|
console.warn(
|
|
864
|
-
|
|
1332
|
+
"[nodeui] running with NODE_ENV=production. The console exposes environment, logs and heap data; keep it behind loopback and an auth token."
|
|
865
1333
|
);
|
|
866
1334
|
}
|
|
1335
|
+
if (config.enabled && !config.maskSecrets) {
|
|
1336
|
+
console.warn("[nodeui] secret masking is disabled; panels may show credentials.");
|
|
1337
|
+
}
|
|
867
1338
|
const ctx = { config, env, store: {} };
|
|
868
1339
|
if (options.config !== void 0) {
|
|
869
1340
|
ctx.store["app-config"] = options.config;
|
|
870
1341
|
}
|
|
871
1342
|
const registry = new ProviderRegistry();
|
|
872
|
-
registry.register(new HealthProvider());
|
|
1343
|
+
registry.register(new HealthProvider(options.healthChecks));
|
|
873
1344
|
registry.register(new MemoryProvider());
|
|
874
1345
|
registry.register(new CpuProvider());
|
|
875
1346
|
registry.register(new EventLoopLagProvider());
|
|
@@ -891,9 +1362,23 @@ function createNodeUI(options = {}) {
|
|
|
891
1362
|
registry.register(envProvider);
|
|
892
1363
|
const routesProvider = new RoutesProvider();
|
|
893
1364
|
registry.register(routesProvider);
|
|
1365
|
+
registry.register(
|
|
1366
|
+
new OutgoingProvider(positiveInt(options.outgoingLogSize, 200, "outgoingLogSize"))
|
|
1367
|
+
);
|
|
1368
|
+
const pluginMeta = [];
|
|
1369
|
+
for (const plugin of options.plugins ?? []) {
|
|
1370
|
+
if (!PLUGIN_ID_PATTERN.test(plugin.id) || BUILT_IN_PANELS.has(plugin.id) || RESERVED_API_NAMES.has(plugin.id) || registry.get(plugin.id)) {
|
|
1371
|
+
throw new Error(
|
|
1372
|
+
`nodeui plugin id "${plugin.id}" is invalid or already taken (use lowercase letters, digits and "-")`
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
registry.register(plugin);
|
|
1376
|
+
pluginMeta.push({ id: plugin.id, title: plugin.title ?? plugin.id });
|
|
1377
|
+
}
|
|
894
1378
|
const confirmations = new ConfirmationStore(config.confirmTtlMs);
|
|
895
1379
|
const activeProviders = /* @__PURE__ */ new Set();
|
|
896
1380
|
let inactivityTimer = null;
|
|
1381
|
+
let sseClients = 0;
|
|
897
1382
|
function stopAll() {
|
|
898
1383
|
for (const id of activeProviders) {
|
|
899
1384
|
registry.get(id)?.stop?.(ctx);
|
|
@@ -917,8 +1402,9 @@ function createNodeUI(options = {}) {
|
|
|
917
1402
|
resetInactivityTimer();
|
|
918
1403
|
}
|
|
919
1404
|
function sendJson(res, status, envelope) {
|
|
920
|
-
const body = serializeEnvelope(envelope);
|
|
1405
|
+
const body = serializeEnvelope(envelope, config.maskSecrets);
|
|
921
1406
|
res.writeHead(status, {
|
|
1407
|
+
...SECURITY_HEADERS,
|
|
922
1408
|
"Content-Type": "application/json",
|
|
923
1409
|
"Content-Length": Buffer.byteLength(body),
|
|
924
1410
|
"Cache-Control": "no-store"
|
|
@@ -936,8 +1422,24 @@ function createNodeUI(options = {}) {
|
|
|
936
1422
|
const provider = registry.get(id);
|
|
937
1423
|
if (!provider) return notFound(res);
|
|
938
1424
|
const requestCtx = query ? { ...ctx, query } : ctx;
|
|
939
|
-
|
|
940
|
-
|
|
1425
|
+
sendJson(res, ...await readPanel(provider, requestCtx));
|
|
1426
|
+
}
|
|
1427
|
+
async function readPanel(provider, requestCtx) {
|
|
1428
|
+
try {
|
|
1429
|
+
const result = await provider.get(requestCtx);
|
|
1430
|
+
return [result.ok ? 200 : 500, result];
|
|
1431
|
+
} catch (err) {
|
|
1432
|
+
return [
|
|
1433
|
+
500,
|
|
1434
|
+
{
|
|
1435
|
+
ok: false,
|
|
1436
|
+
error: {
|
|
1437
|
+
code: "provider-failed",
|
|
1438
|
+
message: err instanceof Error ? err.message : "provider failed"
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
];
|
|
1442
|
+
}
|
|
941
1443
|
}
|
|
942
1444
|
function configData() {
|
|
943
1445
|
return {
|
|
@@ -950,7 +1452,9 @@ function createNodeUI(options = {}) {
|
|
|
950
1452
|
logSize: config.logSize,
|
|
951
1453
|
pollIntervalMs: config.pollIntervalMs,
|
|
952
1454
|
panels: registry.ids(),
|
|
953
|
-
masking: { enabled: config.maskSecrets, pattern: SECRET_KEY_PATTERN.source }
|
|
1455
|
+
masking: { enabled: config.maskSecrets, pattern: SECRET_KEY_PATTERN.source },
|
|
1456
|
+
authRequired: config.authRequired,
|
|
1457
|
+
plugins: pluginMeta
|
|
954
1458
|
};
|
|
955
1459
|
}
|
|
956
1460
|
function issueConfirmation(res) {
|
|
@@ -982,22 +1486,37 @@ function createNodeUI(options = {}) {
|
|
|
982
1486
|
const requested = (search.get("panels") ?? "").split(",").filter(Boolean);
|
|
983
1487
|
const panels = (requested.length > 0 ? requested.filter((id) => registry.get(id) !== void 0) : registry.ids()).filter((value, index, self) => self.indexOf(value) === index);
|
|
984
1488
|
if (panels.length === 0) return notFound(res);
|
|
985
|
-
|
|
1489
|
+
if (sseClients >= config.maxSseClients) {
|
|
1490
|
+
res.writeHead(429, { ...SECURITY_HEADERS, "Content-Type": "application/json" });
|
|
1491
|
+
res.end(
|
|
1492
|
+
JSON.stringify({
|
|
1493
|
+
ok: false,
|
|
1494
|
+
error: { code: "too-many-streams", message: "too many open live streams" }
|
|
1495
|
+
})
|
|
1496
|
+
);
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
sseClients += 1;
|
|
1500
|
+
const stream = startSse(res, SECURITY_HEADERS);
|
|
986
1501
|
const push = async () => {
|
|
987
1502
|
for (const id of panels) {
|
|
988
1503
|
ensureActive(id);
|
|
989
1504
|
const provider = registry.get(id);
|
|
990
1505
|
if (!provider) continue;
|
|
991
|
-
const result = await provider
|
|
992
|
-
stream.send({ panel: id, envelope: result });
|
|
1506
|
+
const [, result] = await readPanel(provider, ctx);
|
|
1507
|
+
stream.send({ panel: id, envelope: config.maskSecrets ? maskSecrets(result) : result });
|
|
993
1508
|
}
|
|
994
1509
|
};
|
|
995
|
-
void push();
|
|
996
|
-
const pushTimer = setInterval(() => void push(), config.pollIntervalMs);
|
|
1510
|
+
void push().catch(() => void 0);
|
|
1511
|
+
const pushTimer = setInterval(() => void push().catch(() => void 0), config.pollIntervalMs);
|
|
997
1512
|
const heartbeatTimer = setInterval(() => stream.heartbeat(), SSE_HEARTBEAT_MS);
|
|
998
1513
|
if (typeof pushTimer.unref === "function") pushTimer.unref();
|
|
999
1514
|
if (typeof heartbeatTimer.unref === "function") heartbeatTimer.unref();
|
|
1515
|
+
let cleaned = false;
|
|
1000
1516
|
const cleanup = () => {
|
|
1517
|
+
if (cleaned) return;
|
|
1518
|
+
cleaned = true;
|
|
1519
|
+
sseClients -= 1;
|
|
1001
1520
|
clearInterval(pushTimer);
|
|
1002
1521
|
clearInterval(heartbeatTimer);
|
|
1003
1522
|
stream.close();
|
|
@@ -1040,30 +1559,33 @@ function createNodeUI(options = {}) {
|
|
|
1040
1559
|
return;
|
|
1041
1560
|
}
|
|
1042
1561
|
res.writeHead(200, {
|
|
1562
|
+
...SECURITY_HEADERS,
|
|
1563
|
+
"Content-Security-Policy": CSP,
|
|
1043
1564
|
"Content-Type": asset.contentType,
|
|
1044
1565
|
"Content-Length": asset.length,
|
|
1045
1566
|
"Cache-Control": "no-cache"
|
|
1046
1567
|
});
|
|
1047
1568
|
asset.content.pipe(res);
|
|
1048
1569
|
}
|
|
1049
|
-
function guardLoopback(req, res) {
|
|
1050
|
-
if (isLoopbackAddress(config.host)) {
|
|
1051
|
-
const remote = req.socket.remoteAddress;
|
|
1052
|
-
if (!isLoopbackAddress(remote)) {
|
|
1053
|
-
sendJson(res, 403, {
|
|
1054
|
-
ok: false,
|
|
1055
|
-
error: {
|
|
1056
|
-
code: "forbidden",
|
|
1057
|
-
message: "nodeui is loopback-only; requests from other hosts are rejected"
|
|
1058
|
-
}
|
|
1059
|
-
});
|
|
1060
|
-
return false;
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
return true;
|
|
1064
|
-
}
|
|
1065
1570
|
async function handleNodeUIPath(req, res, urlPath) {
|
|
1066
|
-
|
|
1571
|
+
const verdict = guard(req);
|
|
1572
|
+
if (!verdict.ok) {
|
|
1573
|
+
sendJson(res, verdict.status, {
|
|
1574
|
+
ok: false,
|
|
1575
|
+
error: { code: verdict.code, message: verdict.message }
|
|
1576
|
+
});
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
if (verdict.setToken !== void 0) {
|
|
1580
|
+
res.writeHead(302, {
|
|
1581
|
+
...SECURITY_HEADERS,
|
|
1582
|
+
"Set-Cookie": `${AUTH_COOKIE}=${encodeURIComponent(verdict.setToken)}; Path=${path2}; HttpOnly; SameSite=Strict`,
|
|
1583
|
+
Location: `${path2}/`,
|
|
1584
|
+
"Cache-Control": "no-store"
|
|
1585
|
+
});
|
|
1586
|
+
res.end();
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1067
1589
|
const apiBase = `${path2}/api`;
|
|
1068
1590
|
if (urlPath === apiBase || urlPath.startsWith(`${apiBase}/`)) {
|
|
1069
1591
|
await handleApi(req, res, urlPath.slice(apiBase.length));
|
|
@@ -1147,6 +1669,9 @@ function createNodeUI(options = {}) {
|
|
|
1147
1669
|
}
|
|
1148
1670
|
notFound(res);
|
|
1149
1671
|
},
|
|
1672
|
+
setRoutes(source) {
|
|
1673
|
+
ctx.store["routes-source"] = source;
|
|
1674
|
+
},
|
|
1150
1675
|
mark(name) {
|
|
1151
1676
|
startupTracker.mark(name);
|
|
1152
1677
|
},
|
|
@@ -1163,6 +1688,7 @@ function createNodeUI(options = {}) {
|
|
|
1163
1688
|
return server;
|
|
1164
1689
|
}
|
|
1165
1690
|
export {
|
|
1691
|
+
AUTH_COOKIE,
|
|
1166
1692
|
ConfirmationStore,
|
|
1167
1693
|
CpuProvider,
|
|
1168
1694
|
DEFAULT_LOG_SIZE,
|
|
@@ -1173,6 +1699,7 @@ export {
|
|
|
1173
1699
|
LogsProvider,
|
|
1174
1700
|
MemoryProvider,
|
|
1175
1701
|
MetricsProvider,
|
|
1702
|
+
OutgoingProvider,
|
|
1176
1703
|
ProviderRegistry,
|
|
1177
1704
|
RequestsProvider,
|
|
1178
1705
|
RingBuffer,
|
|
@@ -1181,11 +1708,17 @@ export {
|
|
|
1181
1708
|
SECRET_MASKED,
|
|
1182
1709
|
Sampler,
|
|
1183
1710
|
StartupTracker,
|
|
1711
|
+
createGuard,
|
|
1184
1712
|
createNodeUI,
|
|
1185
1713
|
extractRoutes,
|
|
1714
|
+
hostnameFromHostHeader,
|
|
1186
1715
|
interceptConsole,
|
|
1716
|
+
interceptOutgoing,
|
|
1187
1717
|
isLoopbackAddress,
|
|
1718
|
+
isLoopbackHostname,
|
|
1719
|
+
maskSecretText,
|
|
1188
1720
|
maskSecrets,
|
|
1721
|
+
matchesAddress,
|
|
1189
1722
|
resolveActivation,
|
|
1190
1723
|
resolveStaticAsset,
|
|
1191
1724
|
serializeEnvelope,
|