server-devtools 0.1.6 → 0.1.8
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 +67 -3
- package/dist/dashboard/assets/alert-dialog-DOm6Ua9Q.js +1 -0
- package/dist/dashboard/assets/button-CXALLF_v.js +1 -0
- package/dist/dashboard/assets/index-9FEJIlQV.css +2 -0
- package/dist/dashboard/assets/index-B9G80X8M.js +10 -0
- package/dist/dashboard/assets/overview-page-CZhNooRx.js +53 -0
- package/dist/dashboard/assets/requests-page-DvONd75O.js +1 -0
- package/dist/dashboard/assets/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/dashboard/assets/trace-details-page-Dze7uRsF.js +1 -0
- package/dist/dashboard/assets/with-selector-B8m4Ky1i.js +1 -0
- package/dist/dashboard/index.html +4 -2
- package/dist/src/config/index.d.ts +12 -0
- package/dist/src/config/index.d.ts.map +1 -1
- package/dist/src/config/index.js +21 -2
- package/dist/src/config/index.js.map +1 -1
- package/dist/src/core/repeated-database-queries/index.d.ts +32 -0
- package/dist/src/core/repeated-database-queries/index.d.ts.map +1 -0
- package/dist/src/core/repeated-database-queries/index.js +130 -0
- package/dist/src/core/repeated-database-queries/index.js.map +1 -0
- package/dist/src/core/repeated-database-queries/mongodb.d.ts +2 -0
- package/dist/src/core/repeated-database-queries/mongodb.d.ts.map +1 -0
- package/dist/src/core/repeated-database-queries/mongodb.js +43 -0
- package/dist/src/core/repeated-database-queries/mongodb.js.map +1 -0
- package/dist/src/core/repeated-database-queries/mysql.d.ts +2 -0
- package/dist/src/core/repeated-database-queries/mysql.d.ts.map +1 -0
- package/dist/src/core/repeated-database-queries/mysql.js +16 -0
- package/dist/src/core/repeated-database-queries/mysql.js.map +1 -0
- package/dist/src/core/repeated-database-queries/postgres.d.ts +2 -0
- package/dist/src/core/repeated-database-queries/postgres.d.ts.map +1 -0
- package/dist/src/core/repeated-database-queries/postgres.js +15 -0
- package/dist/src/core/repeated-database-queries/postgres.js.map +1 -0
- package/dist/src/core/repeated-database-queries/redis.d.ts +8 -0
- package/dist/src/core/repeated-database-queries/redis.d.ts.map +1 -0
- package/dist/src/core/repeated-database-queries/redis.js +91 -0
- package/dist/src/core/repeated-database-queries/redis.js.map +1 -0
- package/dist/src/core/repeated-database-queries/sql-template.d.ts +2 -0
- package/dist/src/core/repeated-database-queries/sql-template.d.ts.map +1 -0
- package/dist/src/core/repeated-database-queries/sql-template.js +12 -0
- package/dist/src/core/repeated-database-queries/sql-template.js.map +1 -0
- package/dist/src/core/slow-span.d.ts +5 -0
- package/dist/src/core/slow-span.d.ts.map +1 -0
- package/dist/src/core/slow-span.js +37 -0
- package/dist/src/core/slow-span.js.map +1 -0
- package/dist/src/dashboard/dashboard-server.d.ts +12 -1
- package/dist/src/dashboard/dashboard-server.d.ts.map +1 -1
- package/dist/src/dashboard/dashboard-server.js +261 -25
- package/dist/src/dashboard/dashboard-server.js.map +1 -1
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +29 -4
- package/dist/src/index.js.map +1 -1
- package/dist/src/security/auth.service.d.ts +6 -0
- package/dist/src/security/auth.service.d.ts.map +1 -1
- package/dist/src/security/auth.service.js +62 -4
- package/dist/src/security/auth.service.js.map +1 -1
- package/dist/src/storage/sqlite-storage.d.ts +7 -1
- package/dist/src/storage/sqlite-storage.d.ts.map +1 -1
- package/dist/src/storage/sqlite-storage.js +100 -7
- package/dist/src/storage/sqlite-storage.js.map +1 -1
- package/dist/src/types/dashboard.d.ts +8 -0
- package/dist/src/types/dashboard.d.ts.map +1 -1
- package/dist/src/types/misc.d.ts +2 -0
- package/dist/src/types/misc.d.ts.map +1 -1
- package/dist/src/utils/allowed-ips.d.ts +4 -0
- package/dist/src/utils/allowed-ips.d.ts.map +1 -0
- package/dist/src/utils/allowed-ips.js +34 -0
- package/dist/src/utils/allowed-ips.js.map +1 -0
- package/dist/src/utils/helpers.d.ts +2 -0
- package/dist/src/utils/helpers.d.ts.map +1 -0
- package/dist/src/utils/helpers.js +13 -0
- package/dist/src/utils/helpers.js.map +1 -0
- package/package.json +3 -2
- package/dist/dashboard/assets/index-CmrEW8P5.css +0 -2
- package/dist/dashboard/assets/index-DMrAmgB9.js +0 -61
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.postgresDatabaseGroupKey = postgresDatabaseGroupKey;
|
|
4
|
+
const sql_template_1 = require("./sql-template");
|
|
5
|
+
function postgresDatabaseGroupKey(attributes) {
|
|
6
|
+
if (attributes["db.system.name"] !== "postgresql") {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
const query = attributes["db.query.text"];
|
|
10
|
+
if (typeof query !== "string" || query.trim() === "") {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
return `postgresql:${(0, sql_template_1.normalizeSqlTemplate)(query)}`;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=postgres.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres.js","sourceRoot":"","sources":["../../../../src/core/repeated-database-queries/postgres.ts"],"names":[],"mappings":";;;AAAA,iDAAsD;AAEtD,kCACI,UAAmC;IAEnC,IAAI,UAAU,CAAC,gBAAgB,CAAC,KAAK,YAAY,EAAE,CAAC;QAChD,OAAO,SAAS,CAAC;IACrB,CAAC;IAED,MAAM,KAAK,GAAG,UAAU,CAAC,eAAe,CAAC,CAAC;IAC1C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QACnD,OAAO,SAAS,CAAC;IACrB,CAAC;IAED,OAAO,cAAc,IAAA,mCAAoB,EAAC,KAAK,CAAC,EAAE,CAAC;AACvD,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function parseRedisCommandText(queryText: string): {
|
|
2
|
+
command: string;
|
|
3
|
+
args: string[];
|
|
4
|
+
} | undefined;
|
|
5
|
+
export declare function normalizeRedisCommandPattern(command: string, args: string[]): string;
|
|
6
|
+
export declare function redisDatabaseGroupKey(attributes: Record<string, unknown>): string | undefined;
|
|
7
|
+
export declare function formatRedisCommandPattern(groupKey: string): string;
|
|
8
|
+
//# sourceMappingURL=redis.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis.d.ts","sourceRoot":"","sources":["../../../../src/core/repeated-database-queries/redis.ts"],"names":[],"mappings":"AAwDA,wBAAgB,qBAAqB,CACjC,SAAS,EAAE,MAAM,GAClB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,SAAS,CAgBjD;AAED,wBAAgB,4BAA4B,CACxC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,GACf,MAAM,CAMR;AAED,wBAAgB,qBAAqB,CACjC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACpC,MAAM,GAAG,SAAS,CAoBpB;AAED,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAElE"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseRedisCommandText = parseRedisCommandText;
|
|
4
|
+
exports.normalizeRedisCommandPattern = normalizeRedisCommandPattern;
|
|
5
|
+
exports.redisDatabaseGroupKey = redisDatabaseGroupKey;
|
|
6
|
+
exports.formatRedisCommandPattern = formatRedisCommandPattern;
|
|
7
|
+
const REDIS_COMMANDS_MASKING_ALL_ARGS = new Set([
|
|
8
|
+
"SET",
|
|
9
|
+
"SETEX",
|
|
10
|
+
"SETNX",
|
|
11
|
+
"PSETEX",
|
|
12
|
+
"GETSET",
|
|
13
|
+
"MSET",
|
|
14
|
+
"LPUSH",
|
|
15
|
+
"RPUSH",
|
|
16
|
+
]);
|
|
17
|
+
const REDIS_COMMANDS_WITH_FIELD_ARG = new Set([
|
|
18
|
+
"HGET",
|
|
19
|
+
"HDEL",
|
|
20
|
+
"HEXISTS",
|
|
21
|
+
"HSET",
|
|
22
|
+
]);
|
|
23
|
+
function normalizeRedisArgument(command, argIndex, arg) {
|
|
24
|
+
const cmd = command.toUpperCase();
|
|
25
|
+
if (REDIS_COMMANDS_MASKING_ALL_ARGS.has(cmd)) {
|
|
26
|
+
return "?";
|
|
27
|
+
}
|
|
28
|
+
if (cmd === "HSET") {
|
|
29
|
+
if (argIndex === 0) {
|
|
30
|
+
return "?";
|
|
31
|
+
}
|
|
32
|
+
if (argIndex === 1) {
|
|
33
|
+
return arg.toLowerCase();
|
|
34
|
+
}
|
|
35
|
+
return "?";
|
|
36
|
+
}
|
|
37
|
+
if (REDIS_COMMANDS_WITH_FIELD_ARG.has(cmd)) {
|
|
38
|
+
if (argIndex === 0) {
|
|
39
|
+
return "?";
|
|
40
|
+
}
|
|
41
|
+
if (argIndex === 1) {
|
|
42
|
+
return arg.toLowerCase();
|
|
43
|
+
}
|
|
44
|
+
return "?";
|
|
45
|
+
}
|
|
46
|
+
if (argIndex === 0) {
|
|
47
|
+
return "?";
|
|
48
|
+
}
|
|
49
|
+
return "?";
|
|
50
|
+
}
|
|
51
|
+
function parseRedisCommandText(queryText) {
|
|
52
|
+
const trimmed = queryText.trim();
|
|
53
|
+
if (trimmed === "") {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
const parts = trimmed.split(/\s+/);
|
|
57
|
+
const command = parts[0]?.toUpperCase();
|
|
58
|
+
if (!command) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
command,
|
|
63
|
+
args: parts.slice(1),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function normalizeRedisCommandPattern(command, args) {
|
|
67
|
+
const normalizedArgs = args.map((arg, index) => normalizeRedisArgument(command, index, arg));
|
|
68
|
+
return [command.toUpperCase(), ...normalizedArgs].join(" ").trim();
|
|
69
|
+
}
|
|
70
|
+
function redisDatabaseGroupKey(attributes) {
|
|
71
|
+
if (attributes["db.system.name"] !== "redis") {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
const queryText = attributes["db.query.text"];
|
|
75
|
+
if (typeof queryText === "string" && queryText.trim() !== "") {
|
|
76
|
+
const parsed = parseRedisCommandText(queryText);
|
|
77
|
+
if (parsed) {
|
|
78
|
+
const pattern = normalizeRedisCommandPattern(parsed.command, parsed.args);
|
|
79
|
+
return `redis:${pattern}`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const operation = attributes["db.operation.name"];
|
|
83
|
+
if (typeof operation === "string" && operation.trim() !== "") {
|
|
84
|
+
return `redis:${operation.trim().toUpperCase()}`;
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
function formatRedisCommandPattern(groupKey) {
|
|
89
|
+
return groupKey.startsWith("redis:") ? groupKey.slice("redis:".length) : groupKey;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=redis.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis.js","sourceRoot":"","sources":["../../../../src/core/repeated-database-queries/redis.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql-template.d.ts","sourceRoot":"","sources":["../../../../src/core/repeated-database-queries/sql-template.ts"],"names":[],"mappings":"AAAA,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAO1D"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeSqlTemplate = normalizeSqlTemplate;
|
|
4
|
+
function normalizeSqlTemplate(query) {
|
|
5
|
+
let normalized = query.trim().replace(/\s+/g, " ");
|
|
6
|
+
normalized = normalized.replace(/\$\d+/g, "?");
|
|
7
|
+
normalized = normalized.replace(/'(?:''|[^'])*'/g, "?");
|
|
8
|
+
normalized = normalized.replace(/=\s*-?\d+(?:\.\d+)?/g, "= ?");
|
|
9
|
+
normalized = normalized.replace(/,\s*-?\d+(?:\.\d+)?(?=\s*[),])/g, ", ?");
|
|
10
|
+
return normalized.toLowerCase();
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=sql-template.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql-template.js","sourceRoot":"","sources":["../../../../src/core/repeated-database-queries/sql-template.ts"],"names":[],"mappings":";;;AAAA,8BAAqC,KAAa;IAC9C,IAAI,UAAU,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACnD,UAAU,GAAG,UAAU,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IAC/C,UAAU,GAAG,UAAU,CAAC,OAAO,CAAC,iBAAiB,EAAE,GAAG,CAAC,CAAC;IACxD,UAAU,GAAG,UAAU,CAAC,OAAO,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAAC;IAC/D,UAAU,GAAG,UAAU,CAAC,OAAO,CAAC,iCAAiC,EAAE,KAAK,CAAC,CAAC;IAC1E,OAAO,UAAU,CAAC,WAAW,EAAE,CAAC;AACpC,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const DEFAULT_SLOW_SPAN_THRESHOLD_MS: number;
|
|
2
|
+
export declare function parseSlowSpanThresholdMs(value: unknown, fallback?: number): number;
|
|
3
|
+
export declare function resolveSlowSpanThresholdMs(option?: number): number;
|
|
4
|
+
export declare function isSlowSpan(durationMs: unknown, thresholdMs?: number): boolean;
|
|
5
|
+
//# sourceMappingURL=slow-span.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slow-span.d.ts","sourceRoot":"","sources":["../../../src/core/slow-span.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,8BAA8B,QAAwC,CAAC;AAEpF,wBAAgB,wBAAwB,CACpC,KAAK,EAAE,OAAO,EACd,QAAQ,SAAiC,GAC1C,MAAM,CAaR;AAED,wBAAgB,0BAA0B,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAQlE;AAED,wBAAgB,UAAU,CACtB,UAAU,EAAE,OAAO,EACnB,WAAW,SAAiC,GAC7C,OAAO,CAOT"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DEFAULT_SLOW_SPAN_THRESHOLD_MS = void 0;
|
|
7
|
+
exports.parseSlowSpanThresholdMs = parseSlowSpanThresholdMs;
|
|
8
|
+
exports.resolveSlowSpanThresholdMs = resolveSlowSpanThresholdMs;
|
|
9
|
+
exports.isSlowSpan = isSlowSpan;
|
|
10
|
+
const config_1 = __importDefault(require("../config"));
|
|
11
|
+
exports.DEFAULT_SLOW_SPAN_THRESHOLD_MS = config_1.default.DEFAULT_SLOW_SPAN_THRESHOLD_MS;
|
|
12
|
+
function parseSlowSpanThresholdMs(value, fallback = exports.DEFAULT_SLOW_SPAN_THRESHOLD_MS) {
|
|
13
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
17
|
+
const parsed = Number(value);
|
|
18
|
+
if (Number.isFinite(parsed) && parsed >= 0) {
|
|
19
|
+
return parsed;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return fallback;
|
|
23
|
+
}
|
|
24
|
+
function resolveSlowSpanThresholdMs(option) {
|
|
25
|
+
if (option !== undefined) {
|
|
26
|
+
return parseSlowSpanThresholdMs(option);
|
|
27
|
+
}
|
|
28
|
+
return parseSlowSpanThresholdMs(process.env.SERVER_DEVTOOLS_SLOW_SPAN_THRESHOLD_MS);
|
|
29
|
+
}
|
|
30
|
+
function isSlowSpan(durationMs, thresholdMs = exports.DEFAULT_SLOW_SPAN_THRESHOLD_MS) {
|
|
31
|
+
if (typeof durationMs !== "number" || !Number.isFinite(durationMs) || durationMs < 0) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
const threshold = parseSlowSpanThresholdMs(thresholdMs);
|
|
35
|
+
return durationMs >= threshold;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=slow-span.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slow-span.js","sourceRoot":"","sources":["../../../src/core/slow-span.ts"],"names":[],"mappings":";;;;;;;;;AAAA,uDAA+B;AAElB,QAAA,8BAA8B,GAAG,gBAAM,CAAC,8BAA8B,CAAC;AAEpF,kCACI,KAAc,EACd,QAAQ,GAAG,QAAA,8BAA8B;IAEzC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;QACpE,OAAO,KAAK,CAAC;IACjB,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QACnD,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;QAC7B,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,MAAM,IAAI,CAAC,EAAE,CAAC;YACzC,OAAO,MAAM,CAAC;QAClB,CAAC;IACL,CAAC;IAED,OAAO,QAAQ,CAAC;AACpB,CAAC;AAED,oCAA2C,MAAe;IACtD,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACvB,OAAO,wBAAwB,CAAC,MAAM,CAAC,CAAC;IAC5C,CAAC;IAED,OAAO,wBAAwB,CAC3B,OAAO,CAAC,GAAG,CAAC,sCAAsC,CACrD,CAAC;AACN,CAAC;AAED,oBACI,UAAmB,EACnB,WAAW,GAAG,QAAA,8BAA8B;IAE5C,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,UAAU,GAAG,CAAC,EAAE,CAAC;QACnF,OAAO,KAAK,CAAC;IACjB,CAAC;IAED,MAAM,SAAS,GAAG,wBAAwB,CAAC,WAAW,CAAC,CAAC;IACxD,OAAO,UAAU,IAAI,SAAS,CAAC;AACnC,CAAC"}
|
|
@@ -4,15 +4,26 @@ import AuthService from "../security/auth.service";
|
|
|
4
4
|
export default class DashboardServer {
|
|
5
5
|
private readonly storage;
|
|
6
6
|
private readonly authService;
|
|
7
|
+
private readonly slowSpanThresholdMs;
|
|
8
|
+
private readonly allowedIPs?;
|
|
7
9
|
private readonly executionTreeBuilder;
|
|
8
10
|
private readonly requestDetailMapper;
|
|
9
11
|
private readonly responseDetailMapper;
|
|
10
12
|
private readonly errorMapper;
|
|
11
|
-
|
|
13
|
+
private readonly loginRateLimiter;
|
|
14
|
+
private readonly trackedLoginRateLimitKeys;
|
|
15
|
+
private maxTrackedLoginRateLimitKeys;
|
|
16
|
+
private loginBodyReadTimeoutMs;
|
|
17
|
+
constructor(storage: SQLiteStorage, authService: AuthService, slowSpanThresholdMs?: number, allowedIPs?: Set<string> | undefined);
|
|
18
|
+
private sendIpForbidden;
|
|
12
19
|
private readJsonBody;
|
|
13
20
|
private getCookie;
|
|
14
21
|
private authenticateRequest;
|
|
15
22
|
private sendUnauthorized;
|
|
23
|
+
private reclaimExpiredLoginRateLimitKeys;
|
|
24
|
+
private getLoginRateLimitCapacityRetryAfterMs;
|
|
25
|
+
private trackLoginRateLimitKey;
|
|
26
|
+
private sendLoginRateLimitResponse;
|
|
16
27
|
private handleLogin;
|
|
17
28
|
private handleLogout;
|
|
18
29
|
private handleProfile;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dashboard-server.d.ts","sourceRoot":"","sources":["../../../src/dashboard/dashboard-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAK5D,OAAO,aAGN,MAAM,2BAA2B,CAAC;AAInC,OAAO,WAAW,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"dashboard-server.d.ts","sourceRoot":"","sources":["../../../src/dashboard/dashboard-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAK5D,OAAO,aAGN,MAAM,2BAA2B,CAAC;AAInC,OAAO,WAAW,MAAM,0BAA0B,CAAC;AA2CnD,MAAM,CAAC,OAAO,OAAO,eAAe;IAkB5B,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,mBAAmB;IACpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC;IApBhC,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAA8B;IACnE,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAsC;IAC1E,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAsC;IAC3E,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IACzD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAK9B;IACH,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAA6B;IACvE,OAAO,CAAC,4BAA4B,CAAsD;IAC1F,OAAO,CAAC,sBAAsB,CAA+C;IAI7E,YACqB,OAAO,EAAE,aAAa,EACtB,WAAW,EAAE,WAAW,EACxB,mBAAmB,SAAwC,EAC3D,UAAU,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,YAAA,EAE5C;IAED,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,YAAY;IA8FpB,OAAO,CAAC,SAAS;IAyBjB,OAAO,CAAC,mBAAmB;IAqB3B,OAAO,CAAC,gBAAgB;IAWxB,OAAO,CAAC,gCAAgC;IAQxC,OAAO,CAAC,qCAAqC;IAc7C,OAAO,CAAC,sBAAsB;IAW9B,OAAO,CAAC,0BAA0B;YAgBpB,WAAW;IA+KzB,OAAO,CAAC,YAAY;IAuBpB,OAAO,CAAC,aAAa;IAmCrB,MAAM,CAAC,GAAG,EAAE,eAAe,EACvB,GAAG,EAAE,cAAc,wBAgtBtB;CAGJ"}
|
|
@@ -11,45 +11,149 @@ const dashboard_errors_mapper_1 = __importDefault(require("./dashboard-errors-ma
|
|
|
11
11
|
const dashboard_request_detail_mapper_1 = __importDefault(require("./dashboard-request-detail-mapper"));
|
|
12
12
|
const dashboard_response_detail_mapper_1 = __importDefault(require("./dashboard-response-detail-mapper"));
|
|
13
13
|
const logger_1 = require("../utils/logger");
|
|
14
|
+
const allowed_ips_1 = require("../utils/allowed-ips");
|
|
15
|
+
const helpers_1 = require("../utils/helpers");
|
|
16
|
+
const rate_limiter_flexible_1 = require("rate-limiter-flexible");
|
|
14
17
|
const sourceBuildDashboardDirectory = node_path_1.default.resolve(__dirname, "../../dist/dashboard");
|
|
15
18
|
const dashboardDirectory = (0, node_fs_1.existsSync)(node_path_1.default.join(sourceBuildDashboardDirectory, "index.html"))
|
|
16
19
|
? sourceBuildDashboardDirectory
|
|
17
20
|
: node_path_1.default.resolve(__dirname, "../../dashboard");
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
class LoginBodyTooLargeError extends Error {
|
|
22
|
+
constructor() {
|
|
23
|
+
super("Dashboard login body exceeds the configured limit");
|
|
24
|
+
this.name = "LoginBodyTooLargeError";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
class LoginBodyReadTimeoutError extends Error {
|
|
28
|
+
constructor() {
|
|
29
|
+
super("Dashboard login body read timed out");
|
|
30
|
+
this.name = "LoginBodyReadTimeoutError";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
class LoginBodyClientDisconnectedError extends Error {
|
|
34
|
+
constructor() {
|
|
35
|
+
super("Dashboard login client disconnected");
|
|
36
|
+
this.name = "LoginBodyClientDisconnectedError";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
class LoginBodyInvalidJsonError extends Error {
|
|
40
|
+
constructor() {
|
|
41
|
+
super("Dashboard login body is not valid JSON");
|
|
42
|
+
this.name = "LoginBodyInvalidJsonError";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
26
45
|
class DashboardServer {
|
|
27
46
|
storage;
|
|
28
47
|
authService;
|
|
48
|
+
slowSpanThresholdMs;
|
|
49
|
+
allowedIPs;
|
|
29
50
|
executionTreeBuilder = new execution_tree_builder_1.default();
|
|
30
51
|
requestDetailMapper = new dashboard_request_detail_mapper_1.default();
|
|
31
52
|
responseDetailMapper = new dashboard_response_detail_mapper_1.default();
|
|
32
53
|
errorMapper = new dashboard_errors_mapper_1.default();
|
|
54
|
+
loginRateLimiter = new rate_limiter_flexible_1.RateLimiterMemory({
|
|
55
|
+
keyPrefix: "server-devtools-dashboard-login",
|
|
56
|
+
points: config_1.default.DASHBOARD_LOGIN_RATE_LIMIT.POINTS,
|
|
57
|
+
duration: config_1.default.DASHBOARD_LOGIN_RATE_LIMIT.DURATION_SECONDS,
|
|
58
|
+
blockDuration: config_1.default.DASHBOARD_LOGIN_RATE_LIMIT.BLOCK_SECONDS,
|
|
59
|
+
});
|
|
60
|
+
trackedLoginRateLimitKeys = new Map();
|
|
61
|
+
maxTrackedLoginRateLimitKeys = config_1.default.DASHBOARD_LOGIN_RATE_LIMIT.MAX_TRACKED_KEYS;
|
|
62
|
+
loginBodyReadTimeoutMs = config_1.default.DASHBOARD_LOGIN_BODY_READ_TIMEOUT_MS;
|
|
33
63
|
// private server?: Server;
|
|
34
|
-
constructor(storage, authService) {
|
|
64
|
+
constructor(storage, authService, slowSpanThresholdMs = config_1.default.DEFAULT_SLOW_SPAN_THRESHOLD_MS, allowedIPs) {
|
|
35
65
|
this.storage = storage;
|
|
36
66
|
this.authService = authService;
|
|
67
|
+
this.slowSpanThresholdMs = slowSpanThresholdMs;
|
|
68
|
+
this.allowedIPs = allowedIPs;
|
|
69
|
+
}
|
|
70
|
+
sendIpForbidden(res) {
|
|
71
|
+
res.statusCode = 403;
|
|
72
|
+
res.setHeader("Content-Type", "application/json");
|
|
73
|
+
res.setHeader("Cache-Control", "no-store");
|
|
74
|
+
res.end(JSON.stringify({
|
|
75
|
+
message: "Forbidden",
|
|
76
|
+
}));
|
|
37
77
|
}
|
|
38
|
-
readJsonBody(req) {
|
|
78
|
+
readJsonBody(req, maxBytes, timeoutMs) {
|
|
39
79
|
return new Promise((resolve, reject) => {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
80
|
+
const contentLengthHeader = req.headers["content-length"];
|
|
81
|
+
const contentLengthValue = Array.isArray(contentLengthHeader)
|
|
82
|
+
? contentLengthHeader[0]
|
|
83
|
+
: contentLengthHeader;
|
|
84
|
+
const contentLength = contentLengthValue === undefined
|
|
85
|
+
? undefined
|
|
86
|
+
: Number(contentLengthValue);
|
|
87
|
+
if (contentLength !== undefined &&
|
|
88
|
+
Number.isFinite(contentLength) &&
|
|
89
|
+
contentLength > maxBytes) {
|
|
90
|
+
req.resume();
|
|
91
|
+
reject(new LoginBodyTooLargeError());
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const chunks = [];
|
|
95
|
+
let bytes = 0;
|
|
96
|
+
let settled = false;
|
|
97
|
+
let timeout;
|
|
98
|
+
const cleanup = () => {
|
|
99
|
+
if (timeout) {
|
|
100
|
+
clearTimeout(timeout);
|
|
101
|
+
timeout = undefined;
|
|
102
|
+
}
|
|
103
|
+
req.off("data", onData);
|
|
104
|
+
req.off("end", onEnd);
|
|
105
|
+
req.off("error", onError);
|
|
106
|
+
req.off("aborted", onAborted);
|
|
107
|
+
req.off("close", onClose);
|
|
108
|
+
};
|
|
109
|
+
const fail = (error) => {
|
|
110
|
+
if (settled)
|
|
111
|
+
return;
|
|
112
|
+
settled = true;
|
|
113
|
+
cleanup();
|
|
114
|
+
reject(error);
|
|
115
|
+
};
|
|
116
|
+
const onData = (chunk) => {
|
|
117
|
+
const buffer = Buffer.isBuffer(chunk)
|
|
118
|
+
? chunk
|
|
119
|
+
: Buffer.from(chunk);
|
|
120
|
+
bytes += buffer.byteLength;
|
|
121
|
+
if (bytes > maxBytes) {
|
|
122
|
+
fail(new LoginBodyTooLargeError());
|
|
123
|
+
req.resume();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
chunks.push(buffer);
|
|
127
|
+
};
|
|
128
|
+
const onEnd = () => {
|
|
129
|
+
if (settled)
|
|
130
|
+
return;
|
|
131
|
+
settled = true;
|
|
132
|
+
cleanup();
|
|
45
133
|
try {
|
|
134
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
46
135
|
resolve(body ? JSON.parse(body) : {});
|
|
47
136
|
}
|
|
48
|
-
catch
|
|
49
|
-
reject(
|
|
137
|
+
catch {
|
|
138
|
+
reject(new LoginBodyInvalidJsonError());
|
|
50
139
|
}
|
|
51
|
-
}
|
|
52
|
-
|
|
140
|
+
};
|
|
141
|
+
const onError = () => fail(new LoginBodyClientDisconnectedError());
|
|
142
|
+
const onAborted = () => fail(new LoginBodyClientDisconnectedError());
|
|
143
|
+
const onClose = () => {
|
|
144
|
+
if (!settled) {
|
|
145
|
+
fail(new LoginBodyClientDisconnectedError());
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
req.on("data", onData);
|
|
149
|
+
req.on("end", onEnd);
|
|
150
|
+
req.on("error", onError);
|
|
151
|
+
req.on("aborted", onAborted);
|
|
152
|
+
req.on("close", onClose);
|
|
153
|
+
timeout = setTimeout(() => {
|
|
154
|
+
fail(new LoginBodyReadTimeoutError());
|
|
155
|
+
req.resume();
|
|
156
|
+
}, timeoutMs);
|
|
53
157
|
});
|
|
54
158
|
}
|
|
55
159
|
getCookie(req, name) {
|
|
@@ -60,7 +164,12 @@ class DashboardServer {
|
|
|
60
164
|
for (const cookie of cookies) {
|
|
61
165
|
const [key, ...valueParts] = cookie.trim().split("=");
|
|
62
166
|
if (key === name) {
|
|
63
|
-
|
|
167
|
+
try {
|
|
168
|
+
return decodeURIComponent(valueParts.join("="));
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
64
173
|
}
|
|
65
174
|
}
|
|
66
175
|
return undefined;
|
|
@@ -87,10 +196,114 @@ class DashboardServer {
|
|
|
87
196
|
}));
|
|
88
197
|
return;
|
|
89
198
|
}
|
|
199
|
+
reclaimExpiredLoginRateLimitKeys(now) {
|
|
200
|
+
for (const [key, expiresAt] of this.trackedLoginRateLimitKeys) {
|
|
201
|
+
if (expiresAt <= now) {
|
|
202
|
+
this.trackedLoginRateLimitKeys.delete(key);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
getLoginRateLimitCapacityRetryAfterMs(now) {
|
|
207
|
+
let earliestExpiry = Number.POSITIVE_INFINITY;
|
|
208
|
+
for (const expiresAt of this.trackedLoginRateLimitKeys.values()) {
|
|
209
|
+
earliestExpiry = Math.min(earliestExpiry, expiresAt);
|
|
210
|
+
}
|
|
211
|
+
if (!Number.isFinite(earliestExpiry)) {
|
|
212
|
+
return config_1.default.DASHBOARD_LOGIN_RATE_LIMIT.DURATION_SECONDS * 1000;
|
|
213
|
+
}
|
|
214
|
+
return Math.max(1, earliestExpiry - now);
|
|
215
|
+
}
|
|
216
|
+
trackLoginRateLimitKey(key, msBeforeNext, now = Date.now()) {
|
|
217
|
+
this.trackedLoginRateLimitKeys.set(key, now + Math.max(1, msBeforeNext));
|
|
218
|
+
}
|
|
219
|
+
sendLoginRateLimitResponse(res, msBeforeNext) {
|
|
220
|
+
res.statusCode = 429;
|
|
221
|
+
res.setHeader("Retry-After", Math.max(1, Math.ceil(msBeforeNext / 1000)));
|
|
222
|
+
res.setHeader("Content-Type", "application/json");
|
|
223
|
+
res.setHeader("Cache-Control", "no-store");
|
|
224
|
+
res.end(JSON.stringify({
|
|
225
|
+
message: "Too many login attempts",
|
|
226
|
+
}));
|
|
227
|
+
}
|
|
90
228
|
async handleLogin(req, res) {
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
|
|
229
|
+
const rateLimitKey = (0, helpers_1.normalizeRemoteAddress)(req.socket.remoteAddress);
|
|
230
|
+
const now = Date.now();
|
|
231
|
+
this.reclaimExpiredLoginRateLimitKeys(now);
|
|
232
|
+
const wasTracked = this.trackedLoginRateLimitKeys.has(rateLimitKey);
|
|
233
|
+
if (!wasTracked) {
|
|
234
|
+
if (this.trackedLoginRateLimitKeys.size >= this.maxTrackedLoginRateLimitKeys) {
|
|
235
|
+
this.sendLoginRateLimitResponse(res, this.getLoginRateLimitCapacityRetryAfterMs(now));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
this.trackLoginRateLimitKey(rateLimitKey, config_1.default.DASHBOARD_LOGIN_RATE_LIMIT.DURATION_SECONDS * 1000, now);
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
const consumeResult = await this.loginRateLimiter.consume(rateLimitKey);
|
|
242
|
+
this.trackLoginRateLimitKey(rateLimitKey, consumeResult.msBeforeNext);
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
if (error instanceof rate_limiter_flexible_1.RateLimiterRes) {
|
|
246
|
+
this.trackLoginRateLimitKey(rateLimitKey, error.msBeforeNext);
|
|
247
|
+
this.sendLoginRateLimitResponse(res, error.msBeforeNext);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (!wasTracked) {
|
|
251
|
+
this.trackedLoginRateLimitKeys.delete(rateLimitKey);
|
|
252
|
+
}
|
|
253
|
+
(0, logger_1.debugLog)("Dashboard login rate-limit failed", {
|
|
254
|
+
errorName: error instanceof Error ? error.name : typeof error,
|
|
255
|
+
});
|
|
256
|
+
res.statusCode = 503;
|
|
257
|
+
res.setHeader("Content-Type", "application/json");
|
|
258
|
+
res.setHeader("Cache-Control", "no-store");
|
|
259
|
+
res.end(JSON.stringify({
|
|
260
|
+
message: "Unable to process login request",
|
|
261
|
+
}));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
let body;
|
|
265
|
+
try {
|
|
266
|
+
body = await this.readJsonBody(req, config_1.default.DASHBOARD_LOGIN_BODY_LIMIT_BYTES, this.loginBodyReadTimeoutMs);
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
if (error instanceof LoginBodyTooLargeError) {
|
|
270
|
+
res.statusCode = 413;
|
|
271
|
+
res.setHeader("Content-Type", "application/json");
|
|
272
|
+
res.setHeader("Cache-Control", "no-store");
|
|
273
|
+
res.end(JSON.stringify({
|
|
274
|
+
message: "Login request body is too large",
|
|
275
|
+
}));
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (error instanceof LoginBodyReadTimeoutError) {
|
|
279
|
+
res.statusCode = 408;
|
|
280
|
+
res.setHeader("Content-Type", "application/json");
|
|
281
|
+
res.setHeader("Cache-Control", "no-store");
|
|
282
|
+
res.setHeader("Connection", "close");
|
|
283
|
+
res.end(JSON.stringify({
|
|
284
|
+
message: "Login request timed out",
|
|
285
|
+
}), () => {
|
|
286
|
+
req.destroy();
|
|
287
|
+
res.socket?.destroy();
|
|
288
|
+
});
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (error instanceof LoginBodyClientDisconnectedError) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (error instanceof LoginBodyInvalidJsonError) {
|
|
295
|
+
res.statusCode = 400;
|
|
296
|
+
res.setHeader("Content-Type", "application/json");
|
|
297
|
+
res.setHeader("Cache-Control", "no-store");
|
|
298
|
+
res.end(JSON.stringify({
|
|
299
|
+
message: "Login request body must be valid JSON",
|
|
300
|
+
}));
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
const username = typeof body?.username === "string" ? body.username : null;
|
|
306
|
+
const password = typeof body?.password === "string" ? body.password : null;
|
|
94
307
|
if (!username || !password) {
|
|
95
308
|
res.statusCode = 400;
|
|
96
309
|
res.setHeader("Content-Type", "application/json");
|
|
@@ -121,6 +334,15 @@ class DashboardServer {
|
|
|
121
334
|
res.end(JSON.stringify({ message: "Unable to create session" }));
|
|
122
335
|
return;
|
|
123
336
|
}
|
|
337
|
+
try {
|
|
338
|
+
const rewardResult = await this.loginRateLimiter.reward(rateLimitKey, 1);
|
|
339
|
+
this.trackLoginRateLimitKey(rateLimitKey, rewardResult.msBeforeNext);
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
(0, logger_1.debugLog)("Dashboard login rate-limit refund failed", {
|
|
343
|
+
errorName: error instanceof Error ? error.name : typeof error,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
124
346
|
res.statusCode = 200;
|
|
125
347
|
res.setHeader("Content-Type", "application/json");
|
|
126
348
|
res.setHeader("Set-Cookie", `sdt_session=${token}; HttpOnly; SameSite=Strict; Path=/_devtools`);
|
|
@@ -172,6 +394,10 @@ class DashboardServer {
|
|
|
172
394
|
handle(req, res) {
|
|
173
395
|
const requestUrl = req.url ?? '';
|
|
174
396
|
const pathname = requestUrl.split('?')[0];
|
|
397
|
+
if (!(0, allowed_ips_1.isRequestIpAllowed)(req.socket.remoteAddress, this.allowedIPs)) {
|
|
398
|
+
this.sendIpForbidden(res);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
175
401
|
if (req.method === "POST" &&
|
|
176
402
|
pathname === `${config_1.default.DASHBOARD_API_ROUTES.AUTH}/login`) {
|
|
177
403
|
return this.handleLogin(req, res);
|
|
@@ -191,6 +417,16 @@ class DashboardServer {
|
|
|
191
417
|
pathname === `${config_1.default.DASHBOARD_API_ROUTES.AUTH}/profile`) {
|
|
192
418
|
return this.handleProfile(req, res);
|
|
193
419
|
}
|
|
420
|
+
if (req.method === config_1.default.REQUEST_METHOD.GET &&
|
|
421
|
+
pathname === config_1.default.DASHBOARD_API_ROUTES.CONFIG) {
|
|
422
|
+
res.statusCode = 200;
|
|
423
|
+
res.setHeader("Content-Type", "application/json");
|
|
424
|
+
res.setHeader("Cache-Control", "no-store");
|
|
425
|
+
res.end(JSON.stringify({
|
|
426
|
+
slowSpanThresholdMs: this.slowSpanThresholdMs,
|
|
427
|
+
}));
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
194
430
|
if (req.method === config_1.default.REQUEST_METHOD.DELETE &&
|
|
195
431
|
pathname === config_1.default.DASHBOARD_API_ROUTES.TRACES) {
|
|
196
432
|
try {
|
|
@@ -739,7 +975,7 @@ class DashboardServer {
|
|
|
739
975
|
}
|
|
740
976
|
const content = (0, node_fs_1.readFileSync)(filePath);
|
|
741
977
|
res.statusCode = 200;
|
|
742
|
-
res.setHeader("Content-Type",
|
|
978
|
+
res.setHeader("Content-Type", config_1.default.DASHBOARD_CONTENT_TYPES[node_path_1.default.extname(filePath)] ?? "application/octet-stream");
|
|
743
979
|
res.end(content);
|
|
744
980
|
return;
|
|
745
981
|
}
|
|
@@ -748,7 +984,7 @@ class DashboardServer {
|
|
|
748
984
|
try {
|
|
749
985
|
const content = (0, node_fs_1.readFileSync)(node_path_1.default.join(dashboardDirectory, "index.html"));
|
|
750
986
|
res.statusCode = 200;
|
|
751
|
-
res.setHeader("Content-Type",
|
|
987
|
+
res.setHeader("Content-Type", config_1.default.DASHBOARD_CONTENT_TYPES[".html"]);
|
|
752
988
|
res.end(content);
|
|
753
989
|
return;
|
|
754
990
|
}
|