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.
Files changed (74) hide show
  1. package/README.md +67 -3
  2. package/dist/dashboard/assets/alert-dialog-DOm6Ua9Q.js +1 -0
  3. package/dist/dashboard/assets/button-CXALLF_v.js +1 -0
  4. package/dist/dashboard/assets/index-9FEJIlQV.css +2 -0
  5. package/dist/dashboard/assets/index-B9G80X8M.js +10 -0
  6. package/dist/dashboard/assets/overview-page-CZhNooRx.js +53 -0
  7. package/dist/dashboard/assets/requests-page-DvONd75O.js +1 -0
  8. package/dist/dashboard/assets/rolldown-runtime-hePW80VL.js +1 -0
  9. package/dist/dashboard/assets/trace-details-page-Dze7uRsF.js +1 -0
  10. package/dist/dashboard/assets/with-selector-B8m4Ky1i.js +1 -0
  11. package/dist/dashboard/index.html +4 -2
  12. package/dist/src/config/index.d.ts +12 -0
  13. package/dist/src/config/index.d.ts.map +1 -1
  14. package/dist/src/config/index.js +21 -2
  15. package/dist/src/config/index.js.map +1 -1
  16. package/dist/src/core/repeated-database-queries/index.d.ts +32 -0
  17. package/dist/src/core/repeated-database-queries/index.d.ts.map +1 -0
  18. package/dist/src/core/repeated-database-queries/index.js +130 -0
  19. package/dist/src/core/repeated-database-queries/index.js.map +1 -0
  20. package/dist/src/core/repeated-database-queries/mongodb.d.ts +2 -0
  21. package/dist/src/core/repeated-database-queries/mongodb.d.ts.map +1 -0
  22. package/dist/src/core/repeated-database-queries/mongodb.js +43 -0
  23. package/dist/src/core/repeated-database-queries/mongodb.js.map +1 -0
  24. package/dist/src/core/repeated-database-queries/mysql.d.ts +2 -0
  25. package/dist/src/core/repeated-database-queries/mysql.d.ts.map +1 -0
  26. package/dist/src/core/repeated-database-queries/mysql.js +16 -0
  27. package/dist/src/core/repeated-database-queries/mysql.js.map +1 -0
  28. package/dist/src/core/repeated-database-queries/postgres.d.ts +2 -0
  29. package/dist/src/core/repeated-database-queries/postgres.d.ts.map +1 -0
  30. package/dist/src/core/repeated-database-queries/postgres.js +15 -0
  31. package/dist/src/core/repeated-database-queries/postgres.js.map +1 -0
  32. package/dist/src/core/repeated-database-queries/redis.d.ts +8 -0
  33. package/dist/src/core/repeated-database-queries/redis.d.ts.map +1 -0
  34. package/dist/src/core/repeated-database-queries/redis.js +91 -0
  35. package/dist/src/core/repeated-database-queries/redis.js.map +1 -0
  36. package/dist/src/core/repeated-database-queries/sql-template.d.ts +2 -0
  37. package/dist/src/core/repeated-database-queries/sql-template.d.ts.map +1 -0
  38. package/dist/src/core/repeated-database-queries/sql-template.js +12 -0
  39. package/dist/src/core/repeated-database-queries/sql-template.js.map +1 -0
  40. package/dist/src/core/slow-span.d.ts +5 -0
  41. package/dist/src/core/slow-span.d.ts.map +1 -0
  42. package/dist/src/core/slow-span.js +37 -0
  43. package/dist/src/core/slow-span.js.map +1 -0
  44. package/dist/src/dashboard/dashboard-server.d.ts +12 -1
  45. package/dist/src/dashboard/dashboard-server.d.ts.map +1 -1
  46. package/dist/src/dashboard/dashboard-server.js +261 -25
  47. package/dist/src/dashboard/dashboard-server.js.map +1 -1
  48. package/dist/src/index.d.ts +2 -1
  49. package/dist/src/index.d.ts.map +1 -1
  50. package/dist/src/index.js +29 -4
  51. package/dist/src/index.js.map +1 -1
  52. package/dist/src/security/auth.service.d.ts +6 -0
  53. package/dist/src/security/auth.service.d.ts.map +1 -1
  54. package/dist/src/security/auth.service.js +62 -4
  55. package/dist/src/security/auth.service.js.map +1 -1
  56. package/dist/src/storage/sqlite-storage.d.ts +7 -1
  57. package/dist/src/storage/sqlite-storage.d.ts.map +1 -1
  58. package/dist/src/storage/sqlite-storage.js +100 -7
  59. package/dist/src/storage/sqlite-storage.js.map +1 -1
  60. package/dist/src/types/dashboard.d.ts +8 -0
  61. package/dist/src/types/dashboard.d.ts.map +1 -1
  62. package/dist/src/types/misc.d.ts +2 -0
  63. package/dist/src/types/misc.d.ts.map +1 -1
  64. package/dist/src/utils/allowed-ips.d.ts +4 -0
  65. package/dist/src/utils/allowed-ips.d.ts.map +1 -0
  66. package/dist/src/utils/allowed-ips.js +34 -0
  67. package/dist/src/utils/allowed-ips.js.map +1 -0
  68. package/dist/src/utils/helpers.d.ts +2 -0
  69. package/dist/src/utils/helpers.d.ts.map +1 -0
  70. package/dist/src/utils/helpers.js +13 -0
  71. package/dist/src/utils/helpers.js.map +1 -0
  72. package/package.json +3 -2
  73. package/dist/dashboard/assets/index-CmrEW8P5.css +0 -2
  74. 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,2 @@
1
+ export declare function normalizeSqlTemplate(query: string): string;
2
+ //# sourceMappingURL=sql-template.d.ts.map
@@ -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
- constructor(storage: SQLiteStorage, authService: AuthService);
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;AAkBnD,MAAM,CAAC,OAAO,OAAO,eAAe;IAS5B,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAThC,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;IAIzD,YACqB,OAAO,EAAE,aAAa,EACtB,WAAW,EAAE,WAAW,EAE5C;IAED,OAAO,CAAC,YAAY;IAsBpB,OAAO,CAAC,SAAS;IAqBjB,OAAO,CAAC,mBAAmB;IAqB3B,OAAO,CAAC,gBAAgB;YAWV,WAAW;IA0DzB,OAAO,CAAC,YAAY;IAuBpB,OAAO,CAAC,aAAa;IAmCrB,MAAM,CAAC,GAAG,EAAE,eAAe,EACvB,GAAG,EAAE,cAAc,wBA8rBtB;CAGJ"}
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
- const contentTypes = {
19
- ".html": "text/html; charset=utf-8",
20
- ".js": "text/javascript; charset=utf-8",
21
- ".css": "text/css; charset=utf-8",
22
- ".svg": "image/svg+xml",
23
- ".woff2": "font/woff2",
24
- ".json": "application/json; charset=utf-8",
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
- let body = "";
41
- req.on("data", (chunk) => {
42
- body += chunk.toString();
43
- });
44
- req.on("end", () => {
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 (error) {
49
- reject(error);
137
+ catch {
138
+ reject(new LoginBodyInvalidJsonError());
50
139
  }
51
- });
52
- req.on("error", reject);
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
- return decodeURIComponent(valueParts.join("="));
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 body = await this.readJsonBody(req);
92
- const username = body?.username || null;
93
- const password = body?.password || null;
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", contentTypes[node_path_1.default.extname(filePath)] ?? "application/octet-stream");
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", contentTypes[".html"]);
987
+ res.setHeader("Content-Type", config_1.default.DASHBOARD_CONTENT_TYPES[".html"]);
752
988
  res.end(content);
753
989
  return;
754
990
  }