adonisjs-server-stats 1.13.0 → 1.14.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.
Files changed (150) hide show
  1. package/README.md +3 -2
  2. package/dist/configure.js +6 -1
  3. package/dist/core/collectors/collector.d.ts +83 -0
  4. package/dist/core/core/types.d.ts +186 -0
  5. package/dist/core/debug/types.d.ts +467 -0
  6. package/dist/core/index.js +184 -169
  7. package/dist/core/types.d.ts +855 -167
  8. package/dist/react/{CacheSection-CX2duJuc.js → CacheSection-DkLtukWz.js} +1 -1
  9. package/dist/react/{CacheTab-CgT4t0oZ.js → CacheTab-DWeXnEBI.js} +51 -46
  10. package/dist/react/ConfigContent-Bwvfl_G7.js +350 -0
  11. package/dist/react/{ConfigSection-Dt0FyeaW.js → ConfigSection-DaVbFPDg.js} +2 -2
  12. package/dist/react/{ConfigTab-KtiTtsrj.js → ConfigTab-D4640WJZ.js} +2 -2
  13. package/dist/react/{CustomPaneTab-15QoEplP.js → CustomPaneTab-DglmAVMC.js} +1 -1
  14. package/dist/react/{EmailsSection-YcJYR5eA.js → EmailsSection-vYx6ExTb.js} +1 -1
  15. package/dist/react/{EmailsTab-CUjubln_.js → EmailsTab-DKDrRmRH.js} +54 -49
  16. package/dist/react/{EventsSection-GGPoYSNv.js → EventsSection-CapH5xut.js} +1 -1
  17. package/dist/react/{EventsTab-Wdwr0_ny.js → EventsTab-BNVEySAm.js} +1 -1
  18. package/dist/react/{InternalsContent-BNOnSoi9.js → InternalsContent-C9lIA92C.js} +1 -1
  19. package/dist/react/{InternalsSection-BwrTfpjA.js → InternalsSection-lOv_UcSV.js} +1 -1
  20. package/dist/react/{InternalsTab--RD-L1dX.js → InternalsTab-BygoSorC.js} +1 -1
  21. package/dist/react/{JobsSection-BqqIh_DR.js → JobsSection-ZIQsJWid.js} +1 -1
  22. package/dist/react/{JobsTab-CBrU-ryL.js → JobsTab-DXQzXrDt.js} +1 -1
  23. package/dist/react/{LogEntryRow-BOrRkhRU.js → LogEntryRow-BWkHE51-.js} +1 -1
  24. package/dist/react/{LogsSection-Cm_lphM6.js → LogsSection-ConXdBkL.js} +2 -2
  25. package/dist/react/LogsTab-CJM47LPn.js +82 -0
  26. package/dist/react/{OverviewSection-XF7bakyM.js → OverviewSection-DOMu2qvl.js} +131 -131
  27. package/dist/react/{QueriesSection-DsQBKrNK.js → QueriesSection-DFsaOSJI.js} +1 -1
  28. package/dist/react/{QueriesTab-CxCC1GVq.js → QueriesTab-CY9CG_7L.js} +1 -1
  29. package/dist/react/RequestsSection-CC-eVHsl.js +326 -0
  30. package/dist/react/{RoutesSection-D0y5JQP4.js → RoutesSection-p1DMq41y.js} +1 -1
  31. package/dist/react/{RoutesTab-Y_alJVMV.js → RoutesTab-Cnqy8UcK.js} +1 -1
  32. package/dist/react/{SplitPaneWrapper-CZl1ouIT.js → SplitPaneWrapper-XgkA0QxE.js} +1 -1
  33. package/dist/react/{TimelineTab-HyqZpfbp.js → TimelineTab-1YOERxe5.js} +3 -3
  34. package/dist/react/index-DOSlCpZ9.js +1159 -0
  35. package/dist/react/index.js +1 -1
  36. package/dist/react/useApiClient-CtEG7rHs.js +11 -0
  37. package/dist/src/collectors/queue_collector.js +22 -6
  38. package/dist/src/config/deprecation_migration.js +11 -0
  39. package/dist/src/dashboard/cache_handlers.js +22 -2
  40. package/dist/src/dashboard/flush_manager.d.ts +2 -0
  41. package/dist/src/dashboard/flush_manager.js +17 -0
  42. package/dist/src/dashboard/integrations/config_inspector.d.ts +0 -1
  43. package/dist/src/dashboard/integrations/config_inspector.js +4 -2
  44. package/dist/src/dashboard/paginate_helper.d.ts +5 -0
  45. package/dist/src/dashboard/paginate_helper.js +16 -4
  46. package/dist/src/dashboard/query_explain_handler.d.ts +6 -1
  47. package/dist/src/dashboard/query_explain_handler.js +12 -2
  48. package/dist/src/dashboard/write_queue.d.ts +13 -0
  49. package/dist/src/dashboard/write_queue.js +34 -2
  50. package/dist/src/debug/debug_store.js +8 -1
  51. package/dist/src/debug/event_collector.js +4 -0
  52. package/dist/src/debug/query_collector.js +2 -0
  53. package/dist/src/debug/ring_buffer.js +8 -1
  54. package/dist/src/debug/trace_collector.js +34 -10
  55. package/dist/src/debug/types.d.ts +2 -0
  56. package/dist/src/define_config.js +1 -0
  57. package/dist/src/edge/client/dashboard.js +2 -2
  58. package/dist/src/edge/client/debug-panel-deferred.js +1 -1
  59. package/dist/src/edge/client/stats-bar.js +1 -1
  60. package/dist/src/edge/client-vue/dashboard.js +5 -5
  61. package/dist/src/edge/client-vue/debug-panel-deferred.js +2 -2
  62. package/dist/src/engine/request_metrics.js +8 -10
  63. package/dist/src/log_stream/log_stream_service.d.ts +1 -0
  64. package/dist/src/log_stream/log_stream_service.js +7 -5
  65. package/dist/src/prometheus/prometheus_collector.d.ts +9 -0
  66. package/dist/src/prometheus/prometheus_collector.js +11 -0
  67. package/dist/src/provider/dashboard_init.js +15 -0
  68. package/dist/src/provider/dashboard_setup.js +9 -2
  69. package/dist/src/provider/email_bridge.js +6 -2
  70. package/dist/src/provider/email_helpers.d.ts +9 -1
  71. package/dist/src/provider/email_helpers.js +32 -4
  72. package/dist/src/provider/pino_hook.d.ts +8 -0
  73. package/dist/src/provider/pino_hook.js +19 -1
  74. package/dist/src/provider/provider_helpers_extra.d.ts +2 -0
  75. package/dist/src/provider/provider_helpers_extra.js +17 -2
  76. package/dist/src/provider/server_stats_provider.js +19 -1
  77. package/dist/src/provider/shutdown_helpers.js +2 -0
  78. package/dist/src/provider/toolbar_setup.js +17 -14
  79. package/dist/src/routes/dashboard_routes.js +96 -62
  80. package/dist/src/routes/debug_routes.js +45 -35
  81. package/dist/src/routes/no_session_middleware.d.ts +6 -3
  82. package/dist/src/routes/no_session_middleware.js +7 -3
  83. package/dist/src/routes/register_routes.d.ts +8 -0
  84. package/dist/src/routes/register_routes.js +19 -0
  85. package/dist/src/types.d.ts +31 -0
  86. package/dist/src/utils/app_import.js +12 -4
  87. package/dist/vue/CacheSection-CxEBVVkF.js +156 -0
  88. package/dist/vue/ConfigSection-BiRAiaHj.js +415 -0
  89. package/dist/vue/EmailsSection-Dl44qyqY.js +208 -0
  90. package/dist/vue/{EmailsTab-CwIF1fik.js → EmailsTab-Diya54CP.js} +6 -5
  91. package/dist/vue/{EventsSection-BpgkWIwM.js → EventsSection-CWjeitjU.js} +1 -1
  92. package/dist/vue/{InternalsTab-521fxYYj.js → InternalsTab-Z3c82glB.js} +15 -15
  93. package/dist/vue/{JobsSection-DrghFEKL.js → JobsSection-DOBb4LjZ.js} +1 -1
  94. package/dist/vue/LogsSection-CXx-HOWJ.js +260 -0
  95. package/dist/vue/{OverviewSection-BAgZTPjY.js → OverviewSection-CyfNQ8uV.js} +318 -302
  96. package/dist/vue/{QueriesSection-CUpwhp7u.js → QueriesSection-CXBsFp-y.js} +1 -1
  97. package/dist/vue/{RequestsSection-D8P2xpF2.js → RequestsSection-YTIaZGZd.js} +151 -145
  98. package/dist/vue/{RoutesSection-0qB81hTT.js → RoutesSection-CDKMey49.js} +1 -1
  99. package/dist/vue/composables/useApiClient.d.ts +8 -1
  100. package/dist/vue/index-14x39RI_.js +1239 -0
  101. package/dist/vue/index.js +1 -1
  102. package/dist/vue/style.css +1 -1
  103. package/package.json +6 -3
  104. package/dist/react/ConfigContent-CnsEI4j3.js +0 -397
  105. package/dist/react/LogsTab-jKwv9G7Q.js +0 -79
  106. package/dist/react/RequestsSection-D8cMbZU0.js +0 -321
  107. package/dist/react/index-CsprmgzI.js +0 -1121
  108. package/dist/react/useApiClient-BVtNCmnL.js +0 -9
  109. package/dist/vue/CacheSection-DRqV3YX2.js +0 -149
  110. package/dist/vue/ConfigSection-C6pQCHAL.js +0 -576
  111. package/dist/vue/EmailsSection-BTNw3ZU2.js +0 -206
  112. package/dist/vue/LogsSection-BDxx9Bfi.js +0 -258
  113. package/dist/vue/index-30jLw-_w.js +0 -1232
  114. /package/dist/core/{api-client.d.ts → core/api-client.d.ts} +0 -0
  115. /package/dist/core/{config-utils.d.ts → core/config-utils.d.ts} +0 -0
  116. /package/dist/core/{constants.d.ts → core/constants.d.ts} +0 -0
  117. /package/dist/core/{dashboard-api.d.ts → core/dashboard-api.d.ts} +0 -0
  118. /package/dist/core/{dashboard-data-controller.d.ts → core/dashboard-data-controller.d.ts} +0 -0
  119. /package/dist/core/{dashboard-data-helpers.d.ts → core/dashboard-data-helpers.d.ts} +0 -0
  120. /package/dist/core/{debug-data-controller.d.ts → core/debug-data-controller.d.ts} +0 -0
  121. /package/dist/core/{define-config-helpers.d.ts → core/define-config-helpers.d.ts} +0 -0
  122. /package/dist/core/{explain-utils.d.ts → core/explain-utils.d.ts} +0 -0
  123. /package/dist/core/{feature-detect-helpers.d.ts → core/feature-detect-helpers.d.ts} +0 -0
  124. /package/dist/core/{feature-detect.d.ts → core/feature-detect.d.ts} +0 -0
  125. /package/dist/core/{field-resolvers.d.ts → core/field-resolvers.d.ts} +0 -0
  126. /package/dist/core/{formatters-helpers.d.ts → core/formatters-helpers.d.ts} +0 -0
  127. /package/dist/core/{formatters.d.ts → core/formatters.d.ts} +0 -0
  128. /package/dist/core/{history-buffer.d.ts → core/history-buffer.d.ts} +0 -0
  129. /package/dist/core/{icons.d.ts → core/icons.d.ts} +0 -0
  130. /package/dist/core/{index.d.ts → core/index.d.ts} +0 -0
  131. /package/dist/core/{internals-utils.d.ts → core/internals-utils.d.ts} +0 -0
  132. /package/dist/core/{job-utils.d.ts → core/job-utils.d.ts} +0 -0
  133. /package/dist/core/{log-utils-helpers.d.ts → core/log-utils-helpers.d.ts} +0 -0
  134. /package/dist/core/{log-utils.d.ts → core/log-utils.d.ts} +0 -0
  135. /package/dist/core/{metrics.d.ts → core/metrics.d.ts} +0 -0
  136. /package/dist/core/{pagination.d.ts → core/pagination.d.ts} +0 -0
  137. /package/dist/core/{queries-columns.d.ts → core/queries-columns.d.ts} +0 -0
  138. /package/dist/core/{queries-controller.d.ts → core/queries-controller.d.ts} +0 -0
  139. /package/dist/core/{query-utils.d.ts → core/query-utils.d.ts} +0 -0
  140. /package/dist/core/{resizable-columns.d.ts → core/resizable-columns.d.ts} +0 -0
  141. /package/dist/core/{routes.d.ts → core/routes.d.ts} +0 -0
  142. /package/dist/core/{server-stats-controller.d.ts → core/server-stats-controller.d.ts} +0 -0
  143. /package/dist/core/{sparkline.d.ts → core/sparkline.d.ts} +0 -0
  144. /package/dist/core/{split-pane.d.ts → core/split-pane.d.ts} +0 -0
  145. /package/dist/core/{theme.d.ts → core/theme.d.ts} +0 -0
  146. /package/dist/core/{trace-utils.d.ts → core/trace-utils.d.ts} +0 -0
  147. /package/dist/core/{transmit-adapter.d.ts → core/transmit-adapter.d.ts} +0 -0
  148. /package/dist/core/{transmit-helpers.d.ts → core/transmit-helpers.d.ts} +0 -0
  149. /package/dist/core/{types-dashboard.d.ts → core/types-dashboard.d.ts} +0 -0
  150. /package/dist/core/{types-diagnostics.d.ts → core/types-diagnostics.d.ts} +0 -0
@@ -1,9 +1,33 @@
1
1
  import { log } from '../utils/logger.js';
2
- let _whenReady;
3
- function bindDash(getController, method) {
2
+ import { clampPerPage } from '../dashboard/paginate_helper.js';
3
+ /**
4
+ * Lightweight CSRF guard for state-changing routes. Since `noSessionMiddleware`
5
+ * strips `Set-Cookie`, session-based CSRF tokens can't apply — instead we require
6
+ * the request to be provably same-origin (via the `Sec-Fetch-Site` fetch-metadata
7
+ * header) or to carry an `X-Requested-With` header, neither of which a cross-site
8
+ * form/navigation can forge. Returns true when the request is allowed.
9
+ */
10
+ function isSameOriginRequest(ctx) {
11
+ const fetchSite = ctx.request.header('sec-fetch-site');
12
+ if (fetchSite)
13
+ return fetchSite === 'same-origin' || fetchSite === 'none';
14
+ // Older clients without fetch metadata: require an explicit XHR marker,
15
+ // which a cross-site simple form submission cannot set.
16
+ return !!ctx.request.header('x-requested-with');
17
+ }
18
+ /** Wrap a dashboard handler so it rejects cross-site state-changing requests. */
19
+ function guardCsrf(handler) {
20
+ return async (ctx) => {
21
+ if (!isSameOriginRequest(ctx)) {
22
+ return ctx.response.forbidden({ error: 'Cross-site request blocked' });
23
+ }
24
+ return handler(ctx);
25
+ };
26
+ }
27
+ function bindDash(getController, method, whenReady) {
4
28
  return async (ctx) => {
5
- if (_whenReady)
6
- await _whenReady();
29
+ if (whenReady)
30
+ await whenReady();
7
31
  const controller = getController();
8
32
  if (!controller) {
9
33
  log.warn(`503 on ${ctx.request.url()} — dashboardController is null (method: ${method}). ` +
@@ -15,10 +39,10 @@ function bindDash(getController, method) {
15
39
  return controller[method].call(controller, ctx);
16
40
  };
17
41
  }
18
- function bindApi(getApi, fn) {
42
+ function bindApi(getApi, fn, whenReady) {
19
43
  return async (ctx) => {
20
- if (_whenReady)
21
- await _whenReady();
44
+ if (whenReady)
45
+ await whenReady();
22
46
  const api = getApi();
23
47
  if (!api) {
24
48
  log.warn(`503 on ${ctx.request.url()} — apiController is null. ` +
@@ -30,13 +54,13 @@ function bindApi(getApi, fn) {
30
54
  return fn(api, ctx);
31
55
  };
32
56
  }
33
- function registerQueryRoutes(router, getApi) {
57
+ function registerQueryRoutes(router, getApi, whenReady) {
34
58
  router
35
59
  .get('/api/queries', bindApi(getApi, async (api, ctx) => {
36
60
  const qs = ctx.request.qs();
37
61
  return ctx.response.json(await api.getQueries({
38
62
  page: Number(qs.page) || 1,
39
- perPage: Number(qs.perPage) || 25,
63
+ perPage: clampPerPage(Number(qs.perPage) || 25),
40
64
  search: qs.search || undefined,
41
65
  filters: {
42
66
  durationMin: qs.duration_min ? Number(qs.duration_min) : undefined,
@@ -45,50 +69,50 @@ function registerQueryRoutes(router, getApi) {
45
69
  connection: qs.connection || undefined,
46
70
  },
47
71
  }));
48
- }))
72
+ }, whenReady))
49
73
  .as('server-stats.queries');
50
74
  }
51
- function registerEventRoutes(router, getApi) {
75
+ function registerEventRoutes(router, getApi, whenReady) {
52
76
  router
53
77
  .get('/api/events', bindApi(getApi, async (api, ctx) => {
54
78
  const qs = ctx.request.qs();
55
79
  return ctx.response.json(await api.getEvents({
56
80
  page: Number(qs.page) || 1,
57
- perPage: Number(qs.perPage) || 25,
81
+ perPage: clampPerPage(Number(qs.perPage) || 25),
58
82
  search: qs.search || undefined,
59
83
  filters: { eventName: qs.event_name || undefined },
60
84
  }));
61
- }))
85
+ }, whenReady))
62
86
  .as('server-stats.events');
63
87
  }
64
- function registerRouteApiRoutes(router, getApi) {
88
+ function registerRouteApiRoutes(router, getApi, whenReady) {
65
89
  router
66
- .get('/api/routes', bindApi(getApi, (api, ctx) => ctx.response.json(api.getRoutes(ctx.request.qs().search || undefined))))
90
+ .get('/api/routes', bindApi(getApi, (api, ctx) => ctx.response.json(api.getRoutes(ctx.request.qs().search || undefined)), whenReady))
67
91
  .as('server-stats.routes');
68
92
  }
69
- function registerLogRoutes(router, getApi) {
93
+ function registerLogRoutes(router, getApi, whenReady) {
70
94
  router
71
95
  .get('/api/logs', bindApi(getApi, async (api, ctx) => {
72
96
  const qs = ctx.request.qs();
73
97
  return ctx.response.json(await api.getLogs({
74
98
  page: Number(qs.page) || 1,
75
- perPage: Number(qs.perPage) || 50,
99
+ perPage: clampPerPage(Number(qs.perPage) || 50),
76
100
  search: qs.message || qs.search || undefined,
77
101
  filters: {
78
102
  level: qs.level || undefined,
79
103
  requestId: qs.request_id || qs.requestId || undefined,
80
104
  },
81
105
  }));
82
- }))
106
+ }, whenReady))
83
107
  .as('server-stats.logs');
84
108
  }
85
- function registerEmailRoutes(router, getApi) {
109
+ function registerEmailRoutes(router, getApi, whenReady) {
86
110
  router
87
111
  .get('/api/emails', bindApi(getApi, async (api, ctx) => {
88
112
  const qs = ctx.request.qs();
89
113
  return ctx.response.json(await api.getEmails({
90
114
  page: Number(qs.page) || 1,
91
- perPage: Number(qs.perPage) || 25,
115
+ perPage: clampPerPage(Number(qs.perPage) || 25),
92
116
  search: qs.search || undefined,
93
117
  filters: {
94
118
  from: qs.from || undefined,
@@ -98,24 +122,29 @@ function registerEmailRoutes(router, getApi) {
98
122
  mailer: qs.mailer || undefined,
99
123
  },
100
124
  }));
101
- }))
125
+ }, whenReady))
102
126
  .as('server-stats.emails');
103
127
  router
104
128
  .get('/api/emails/:id/preview', bindApi(getApi, async (api, ctx) => {
105
129
  const html = await api.getEmailPreview(Number(ctx.params.id));
106
130
  if (!html)
107
131
  return ctx.response.notFound({ error: 'Email not found' });
108
- return ctx.response.header('Content-Type', 'text/html; charset=utf-8').send(html);
109
- }))
132
+ return ctx.response
133
+ .header('Content-Type', 'text/html; charset=utf-8')
134
+ .header('Content-Security-Policy', "default-src 'none'; style-src 'unsafe-inline'")
135
+ .header('X-Content-Type-Options', 'nosniff')
136
+ .header('X-Frame-Options', 'SAMEORIGIN')
137
+ .send(html);
138
+ }, whenReady))
110
139
  .as('server-stats.emails.preview');
111
140
  }
112
- function registerTraceRoutes(router, getApi) {
141
+ function registerTraceRoutes(router, getApi, whenReady) {
113
142
  router
114
143
  .get('/api/traces', bindApi(getApi, async (api, ctx) => {
115
144
  const qs = ctx.request.qs();
116
145
  return ctx.response.json(await api.getTraces({
117
146
  page: Number(qs.page) || 1,
118
- perPage: Number(qs.perPage) || 25,
147
+ perPage: clampPerPage(Number(qs.perPage) || 25),
119
148
  search: qs.search || undefined,
120
149
  filters: {
121
150
  method: qs.method ? qs.method.toUpperCase() : undefined,
@@ -124,7 +153,7 @@ function registerTraceRoutes(router, getApi) {
124
153
  statusMax: qs.status_max ? Number(qs.status_max) : undefined,
125
154
  },
126
155
  }));
127
- }))
156
+ }, whenReady))
128
157
  .as('server-stats.traces');
129
158
  router
130
159
  .get('/api/traces/:id', bindApi(getApi, async (api, ctx) => {
@@ -132,83 +161,88 @@ function registerTraceRoutes(router, getApi) {
132
161
  if (!trace)
133
162
  return ctx.response.notFound({ error: 'Trace not found' });
134
163
  return ctx.response.json(trace);
135
- }))
164
+ }, whenReady))
136
165
  .as('server-stats.traces.show');
137
166
  }
138
- function registerDashboardPageRoutes(router, getDashboardController) {
139
- router.get('/', bindDash(getDashboardController, 'page')).as('server-stats.dashboard');
167
+ function registerDashboardPageRoutes(router, getDashboardController, whenReady) {
168
+ router.get('/', bindDash(getDashboardController, 'page', whenReady)).as('server-stats.dashboard');
140
169
  router
141
- .get('/api/overview', bindDash(getDashboardController, 'overview'))
170
+ .get('/api/overview', bindDash(getDashboardController, 'overview', whenReady))
142
171
  .as('server-stats.overview');
143
172
  router
144
- .get('/api/overview/chart', bindDash(getDashboardController, 'overviewChart'))
173
+ .get('/api/overview/chart', bindDash(getDashboardController, 'overviewChart', whenReady))
145
174
  .as('server-stats.overview.chart');
146
175
  router
147
- .get('/api/requests', bindDash(getDashboardController, 'requests'))
176
+ .get('/api/requests', bindDash(getDashboardController, 'requests', whenReady))
148
177
  .as('server-stats.requests');
149
178
  router
150
- .get('/api/requests/:id', bindDash(getDashboardController, 'requestDetail'))
179
+ .get('/api/requests/:id', bindDash(getDashboardController, 'requestDetail', whenReady))
151
180
  .as('server-stats.requests.show');
152
181
  }
153
- function registerQueryDashboardRoutes(router, getDashboardController) {
182
+ function registerQueryDashboardRoutes(router, getDashboardController, whenReady) {
154
183
  router
155
- .get('/api/queries/grouped', bindDash(getDashboardController, 'queriesGrouped'))
184
+ .get('/api/queries/grouped', bindDash(getDashboardController, 'queriesGrouped', whenReady))
156
185
  .as('server-stats.queries.grouped');
157
186
  router
158
- .get('/api/queries/:id/explain', bindDash(getDashboardController, 'queryExplain'))
187
+ .get('/api/queries/:id/explain', bindDash(getDashboardController, 'queryExplain', whenReady))
159
188
  .as('server-stats.queries.explain');
160
189
  }
161
- function registerCacheRoutes(router, getDashboardController) {
162
- router.get('/api/cache', bindDash(getDashboardController, 'cacheStats')).as('server-stats.cache');
190
+ function registerCacheRoutes(router, getDashboardController, whenReady) {
191
+ router
192
+ .get('/api/cache', bindDash(getDashboardController, 'cacheStats', whenReady))
193
+ .as('server-stats.cache');
163
194
  router
164
- .get('/api/cache/:key', bindDash(getDashboardController, 'cacheKey'))
195
+ .get('/api/cache/:key', bindDash(getDashboardController, 'cacheKey', whenReady))
165
196
  .as('server-stats.cache.show')
166
197
  .where('key', /.*/);
167
198
  router
168
- .delete('/api/cache/:key', bindDash(getDashboardController, 'cacheKeyDelete'))
199
+ .delete('/api/cache/:key', guardCsrf(bindDash(getDashboardController, 'cacheKeyDelete', whenReady)))
169
200
  .as('server-stats.cache.delete')
170
201
  .where('key', /.*/);
171
202
  }
172
- function registerJobRoutes(router, getDashboardController) {
173
- router.get('/api/jobs', bindDash(getDashboardController, 'jobs')).as('server-stats.jobs');
203
+ function registerJobRoutes(router, getDashboardController, whenReady) {
174
204
  router
175
- .get('/api/jobs/:id', bindDash(getDashboardController, 'jobDetail'))
205
+ .get('/api/jobs', bindDash(getDashboardController, 'jobs', whenReady))
206
+ .as('server-stats.jobs');
207
+ router
208
+ .get('/api/jobs/:id', bindDash(getDashboardController, 'jobDetail', whenReady))
176
209
  .as('server-stats.jobs.show');
177
210
  router
178
- .post('/api/jobs/:id/retry', bindDash(getDashboardController, 'jobRetry'))
211
+ .post('/api/jobs/:id/retry', guardCsrf(bindDash(getDashboardController, 'jobRetry', whenReady)))
179
212
  .as('server-stats.jobs.retry');
180
213
  }
181
- function registerConfigAndFilterRoutes(router, getDashboardController) {
182
- router.get('/api/config', bindDash(getDashboardController, 'config')).as('server-stats.config');
214
+ function registerConfigAndFilterRoutes(router, getDashboardController, whenReady) {
215
+ router
216
+ .get('/api/config', bindDash(getDashboardController, 'config', whenReady))
217
+ .as('server-stats.config');
183
218
  router
184
- .get('/api/filters', bindDash(getDashboardController, 'savedFilters'))
219
+ .get('/api/filters', bindDash(getDashboardController, 'savedFilters', whenReady))
185
220
  .as('server-stats.filters');
186
221
  router
187
- .post('/api/filters', bindDash(getDashboardController, 'createSavedFilter'))
222
+ .post('/api/filters', guardCsrf(bindDash(getDashboardController, 'createSavedFilter', whenReady)))
188
223
  .as('server-stats.filters.create');
189
224
  router
190
- .delete('/api/filters/:id', bindDash(getDashboardController, 'deleteSavedFilter'))
225
+ .delete('/api/filters/:id', guardCsrf(bindDash(getDashboardController, 'deleteSavedFilter', whenReady)))
191
226
  .as('server-stats.filters.delete');
192
227
  }
193
228
  /** Register dashboard routes. */
194
229
  export function registerDashboardRoutes(opts) {
195
230
  const { router, dashboardPath, getDashboardController, getApiController, middleware } = opts;
196
- if (opts.whenReady)
197
- _whenReady = opts.whenReady;
231
+ const whenReady = opts.whenReady;
198
232
  const base = dashboardPath.replace(/\/+$/, '');
199
233
  router
200
234
  .group(() => {
201
- registerDashboardPageRoutes(router, getDashboardController);
202
- registerQueryRoutes(router, getApiController);
203
- registerEventRoutes(router, getApiController);
204
- registerRouteApiRoutes(router, getApiController);
205
- registerLogRoutes(router, getApiController);
206
- registerEmailRoutes(router, getApiController);
207
- registerTraceRoutes(router, getApiController);
208
- registerQueryDashboardRoutes(router, getDashboardController);
209
- registerCacheRoutes(router, getDashboardController);
210
- registerJobRoutes(router, getDashboardController);
211
- registerConfigAndFilterRoutes(router, getDashboardController);
235
+ registerDashboardPageRoutes(router, getDashboardController, whenReady);
236
+ registerQueryRoutes(router, getApiController, whenReady);
237
+ registerEventRoutes(router, getApiController, whenReady);
238
+ registerRouteApiRoutes(router, getApiController, whenReady);
239
+ registerLogRoutes(router, getApiController, whenReady);
240
+ registerEmailRoutes(router, getApiController, whenReady);
241
+ registerTraceRoutes(router, getApiController, whenReady);
242
+ registerQueryDashboardRoutes(router, getDashboardController, whenReady);
243
+ registerCacheRoutes(router, getDashboardController, whenReady);
244
+ registerJobRoutes(router, getDashboardController, whenReady);
245
+ registerConfigAndFilterRoutes(router, getDashboardController, whenReady);
212
246
  })
213
247
  .prefix(base)
214
248
  .use(middleware);
@@ -1,9 +1,8 @@
1
- import { getAppDbClient, buildExplainSql, extractPlan } from '../dashboard/query_explain_handler.js';
2
- let _whenReady;
3
- function bindDebug(getController, method) {
1
+ import { getAppDbClient, buildExplainSql, extractPlan, hasUnsafeSqlTokens, } from '../dashboard/query_explain_handler.js';
2
+ function bindDebug(getController, method, whenReady) {
4
3
  return async (ctx) => {
5
- if (_whenReady)
6
- await _whenReady();
4
+ if (whenReady)
5
+ await whenReady();
7
6
  const controller = getController();
8
7
  if (!controller)
9
8
  return ctx.response.serviceUnavailable({
@@ -12,10 +11,10 @@ function bindDebug(getController, method) {
12
11
  return controller[method].call(controller, ctx);
13
12
  };
14
13
  }
15
- function bindApi(getApi, fn) {
14
+ function bindApi(getApi, fn, whenReady) {
16
15
  return async (ctx) => {
17
- if (_whenReady)
18
- await _whenReady();
16
+ if (whenReady)
17
+ await whenReady();
19
18
  const api = getApi();
20
19
  if (!api)
21
20
  return ctx.response.serviceUnavailable({
@@ -24,62 +23,69 @@ function bindApi(getApi, fn) {
24
23
  return fn(api, ctx);
25
24
  };
26
25
  }
27
- function registerDebugConfigRoutes(router, getDebugController) {
28
- router.get('/config', bindDebug(getDebugController, 'config')).as('server-stats.debug.config');
26
+ function registerDebugConfigRoutes(router, getDebugController, whenReady) {
29
27
  router
30
- .get('/diagnostics', bindDebug(getDebugController, 'diagnostics'))
28
+ .get('/config', bindDebug(getDebugController, 'config', whenReady))
29
+ .as('server-stats.debug.config');
30
+ router
31
+ .get('/diagnostics', bindDebug(getDebugController, 'diagnostics', whenReady))
31
32
  .as('server-stats.debug.diagnostics');
32
33
  }
33
- function registerDebugQueryAndEventRoutes(router, getApiController) {
34
+ function registerDebugQueryAndEventRoutes(router, getApiController, whenReady) {
34
35
  router
35
36
  .get('/queries', bindApi(getApiController, async (api, ctx) => {
36
37
  const queries = await api.getQueries({ source: 'memory' });
37
38
  return ctx.response.json({ queries: queries.data, summary: api.getQuerySummary() });
38
- }))
39
+ }, whenReady))
39
40
  .as('server-stats.debug.queries');
40
41
  router
41
42
  .get('/events', bindApi(getApiController, async (api, ctx) => {
42
43
  const result = await api.getEvents({ source: 'memory' });
43
44
  return ctx.response.json({ events: result.data, total: result.meta.total });
44
- }))
45
+ }, whenReady))
45
46
  .as('server-stats.debug.events');
46
47
  router
47
48
  .get('/routes', bindApi(getApiController, (api, ctx) => {
48
49
  const result = api.getRoutes();
49
50
  return ctx.response.json({ routes: result.data, total: result.meta.total });
50
- }))
51
+ }, whenReady))
51
52
  .as('server-stats.debug.routes');
52
53
  }
53
- function registerDebugLogRoutes(router, getApiController) {
54
+ function registerDebugLogRoutes(router, getApiController, whenReady) {
54
55
  router
55
56
  .get('/logs', bindApi(getApiController, async (api, ctx) => {
56
57
  const result = await api.getLogs({ source: 'auto', perPage: 200 });
57
58
  return ctx.response.json(result.data);
58
- }))
59
+ }, whenReady))
59
60
  .as('server-stats.debug.logs');
60
61
  }
61
- function registerDebugEmailRoutes(router, getApiController) {
62
+ function registerDebugEmailRoutes(router, getApiController, whenReady) {
62
63
  router
63
64
  .get('/emails', bindApi(getApiController, async (api, ctx) => {
64
65
  const result = await api.getEmails({});
65
66
  return ctx.response.json({ emails: result.data, total: result.meta.total });
66
- }))
67
+ }, whenReady))
67
68
  .as('server-stats.debug.emails');
68
69
  router
69
70
  .get('/emails/:id/preview', bindApi(getApiController, async (api, ctx) => {
70
71
  const html = await api.getEmailPreview(Number(ctx.params.id));
71
72
  if (!html)
72
73
  return ctx.response.notFound({ error: 'Email not found' });
73
- return ctx.response.header('Content-Type', 'text/html; charset=utf-8').send(html);
74
- }))
74
+ return ctx.response
75
+ .header('Content-Type', 'text/html; charset=utf-8')
76
+ .header('Content-Security-Policy', "default-src 'none'; style-src 'unsafe-inline'")
77
+ .header('X-Content-Type-Options', 'nosniff')
78
+ .header('X-Frame-Options', 'SAMEORIGIN')
79
+ .send(html);
80
+ }, whenReady))
75
81
  .as('server-stats.debug.emailPreview');
76
82
  }
77
- function registerDebugTraceRoutes(router, getApiController) {
83
+ function registerDebugTraceRoutes(router, getApiController, whenReady) {
78
84
  router
79
85
  .get('/traces', bindApi(getApiController, async (api, ctx) => {
80
86
  const result = await api.getTraces({ source: 'memory' });
81
87
  return ctx.response.json({ traces: result.data, total: result.meta.total });
82
- }))
88
+ }, whenReady))
83
89
  .as('server-stats.debug.traces');
84
90
  router
85
91
  .get('/traces/:id', bindApi(getApiController, async (api, ctx) => {
@@ -87,16 +93,16 @@ function registerDebugTraceRoutes(router, getApiController) {
87
93
  if (!trace)
88
94
  return ctx.response.notFound({ error: 'Trace not found' });
89
95
  return ctx.response.json(trace);
90
- }))
96
+ }, whenReady))
91
97
  .as('server-stats.debug.traceDetail');
92
98
  }
93
- function registerDebugExplainRoute(router, getDebugStore, getApp) {
99
+ function registerDebugExplainRoute(router, whenReady, getDebugStore, getApp) {
94
100
  if (!getDebugStore || !getApp)
95
101
  return;
96
102
  router
97
103
  .get('/queries/:id/explain', async (ctx) => {
98
- if (_whenReady)
99
- await _whenReady();
104
+ if (whenReady)
105
+ await whenReady();
100
106
  const store = getDebugStore();
101
107
  const app = getApp();
102
108
  if (!store || !app) {
@@ -108,6 +114,11 @@ function registerDebugExplainRoute(router, getDebugStore, getApp) {
108
114
  if (!query.sql.trim().toUpperCase().startsWith('SELECT')) {
109
115
  return ctx.response.badRequest({ error: 'EXPLAIN is only supported for SELECT queries' });
110
116
  }
117
+ if (hasUnsafeSqlTokens(query.sql)) {
118
+ return ctx.response.badRequest({
119
+ error: 'EXPLAIN rejected: query contains disallowed tokens',
120
+ });
121
+ }
111
122
  try {
112
123
  const appDb = await getAppDbClient(app, query.connection || undefined);
113
124
  if (!appDb) {
@@ -136,17 +147,16 @@ function registerDebugExplainRoute(router, getDebugStore, getApp) {
136
147
  /** Register debug panel API routes. */
137
148
  export function registerDebugRoutes(opts) {
138
149
  const { router, debugEndpoint, getDebugController, getApiController, middleware } = opts;
139
- if (opts.whenReady)
140
- _whenReady = opts.whenReady;
150
+ const whenReady = opts.whenReady;
141
151
  const base = debugEndpoint.replace(/\/+$/, '');
142
152
  router
143
153
  .group(() => {
144
- registerDebugConfigRoutes(router, getDebugController);
145
- registerDebugQueryAndEventRoutes(router, getApiController);
146
- registerDebugExplainRoute(router, opts.getDebugStore, opts.getApp);
147
- registerDebugLogRoutes(router, getApiController);
148
- registerDebugEmailRoutes(router, getApiController);
149
- registerDebugTraceRoutes(router, getApiController);
154
+ registerDebugConfigRoutes(router, getDebugController, whenReady);
155
+ registerDebugQueryAndEventRoutes(router, getApiController, whenReady);
156
+ registerDebugExplainRoute(router, whenReady, opts.getDebugStore, opts.getApp);
157
+ registerDebugLogRoutes(router, getApiController, whenReady);
158
+ registerDebugEmailRoutes(router, getApiController, whenReady);
159
+ registerDebugTraceRoutes(router, getApiController, whenReady);
150
160
  })
151
161
  .prefix(base)
152
162
  .use(middleware);
@@ -7,8 +7,11 @@ import type { HttpContext } from '@adonisjs/core/http';
7
7
  * Because this package's routes are polled every few seconds, this causes
8
8
  * rapid cookie accumulation that can break the browser.
9
9
  *
10
- * This middleware runs after the route handler (and any global middleware)
11
- * and removes the `Set-Cookie` header so sessions are never started by
12
- * server-stats routes.
10
+ * The `Set-Cookie` header is stripped **before** `next()` runs so it is gone
11
+ * even when a downstream handler flushes headers early (e.g. a streaming/SSE
12
+ * route under these prefixes) — stripping only after `next()` would be a no-op
13
+ * once the response has already been flushed. It is stripped again after
14
+ * `next()` to also cover cookies set by later global middleware on buffered
15
+ * responses.
13
16
  */
14
17
  export declare function noSessionMiddleware(ctx: HttpContext, next: () => Promise<void>): Promise<void>;
@@ -6,11 +6,15 @@
6
6
  * Because this package's routes are polled every few seconds, this causes
7
7
  * rapid cookie accumulation that can break the browser.
8
8
  *
9
- * This middleware runs after the route handler (and any global middleware)
10
- * and removes the `Set-Cookie` header so sessions are never started by
11
- * server-stats routes.
9
+ * The `Set-Cookie` header is stripped **before** `next()` runs so it is gone
10
+ * even when a downstream handler flushes headers early (e.g. a streaming/SSE
11
+ * route under these prefixes) — stripping only after `next()` would be a no-op
12
+ * once the response has already been flushed. It is stripped again after
13
+ * `next()` to also cover cookies set by later global middleware on buffered
14
+ * responses.
12
15
  */
13
16
  export async function noSessionMiddleware(ctx, next) {
17
+ ctx.response.response.removeHeader('set-cookie');
14
18
  await next();
15
19
  ctx.response.response.removeHeader('set-cookie');
16
20
  }
@@ -21,6 +21,14 @@ export interface RegisterRoutesOptions {
21
21
  debugEndpoint?: string;
22
22
  dashboardPath?: string;
23
23
  shouldShow?: (ctx: HttpContext) => boolean;
24
+ /**
25
+ * Escape hatch to register the sensitive dashboard/debug/stats routes WITHOUT
26
+ * any access guard when no `shouldShow` callback is provided. Off by default —
27
+ * without either `shouldShow` or this flag, sensitive routes are NOT registered
28
+ * (fail closed). Enabling this exposes the dashboard (which can surface secrets,
29
+ * email bodies, and SQL) to anyone who can reach it. Local dev only.
30
+ */
31
+ unsafeAllowNoAuth?: boolean;
24
32
  /** Optional promise that resolves when controllers are initialized. */
25
33
  whenReady?: () => Promise<void>;
26
34
  }
@@ -3,10 +3,29 @@ import { registerDashboardRoutes } from './dashboard_routes.js';
3
3
  import { registerDebugRoutes } from './debug_routes.js';
4
4
  import { noSessionMiddleware } from './no_session_middleware.js';
5
5
  import { registerStatsRoute } from './stats_routes.js';
6
+ import { log } from '../utils/logger.js';
7
+ /** One-time warning latch for the unsafeAllowNoAuth escape hatch. */
8
+ let _warnedNoAuth = false;
6
9
  /**
7
10
  * Register all server-stats routes in a single call.
8
11
  */
9
12
  export function registerAllRoutes(options) {
13
+ // Fail closed: without an access guard (`shouldShow`) the sensitive routes must
14
+ // not be registered unless the caller explicitly opts in via `unsafeAllowNoAuth`.
15
+ if (!options.shouldShow) {
16
+ if (!options.unsafeAllowNoAuth) {
17
+ log.warn('server-stats: no `authorize`/`shouldShow` guard configured — sensitive routes ' +
18
+ '(dashboard, debug API, stats) will NOT be registered. Provide an authorize callback, ' +
19
+ 'or set `unsafeAllowNoAuth: true` to expose them without auth (local dev only).');
20
+ return;
21
+ }
22
+ if (!_warnedNoAuth) {
23
+ _warnedNoAuth = true;
24
+ log.warn('server-stats: `unsafeAllowNoAuth` is enabled — the dashboard is exposed WITHOUT ' +
25
+ 'authentication. It can surface secrets, email bodies, and SQL to anyone who can ' +
26
+ 'reach it. Do not use this outside strictly local development.');
27
+ }
28
+ }
10
29
  const middleware = [
11
30
  noSessionMiddleware,
12
31
  ...(options.shouldShow ? [createAccessMiddleware(options.shouldShow)] : []),
@@ -751,6 +751,18 @@ export interface ServerStatsConfig {
751
751
  * ```
752
752
  */
753
753
  authorize?: (ctx: import('@adonisjs/core/http').HttpContext) => boolean;
754
+ /**
755
+ * Register the sensitive dashboard/debug/stats routes WITHOUT any access
756
+ * guard when no {@link authorize} callback is provided.
757
+ *
758
+ * The package fails closed: without either {@link authorize} or this flag,
759
+ * the sensitive routes are **not** registered. Enabling this exposes the
760
+ * dashboard — which can surface secrets, email bodies, and SQL — to anyone
761
+ * who can reach it. Local development only.
762
+ *
763
+ * @default false
764
+ */
765
+ unsafeAllowNoAuth?: boolean;
754
766
  /**
755
767
  * Enable the debug panel (toolbar overlay).
756
768
  *
@@ -838,6 +850,25 @@ export interface ResolvedServerStatsConfig {
838
850
  devToolbar?: DevToolbarOptions;
839
851
  /** Optional access-control callback. */
840
852
  shouldShow?: (ctx: import('@adonisjs/core/http').HttpContext) => boolean;
853
+ /** Collection interval in milliseconds (new name for {@link intervalMs}). */
854
+ pollInterval?: number;
855
+ /** Whether real-time (SSE) broadcasting is enabled (new name for {@link transport}). */
856
+ realtime?: boolean;
857
+ /** HTTP endpoint path or `false` to disable (new name for {@link endpoint}). */
858
+ statsEndpoint?: string | false;
859
+ /** Access-control callback (new name for {@link shouldShow}). */
860
+ authorize?: (ctx: import('@adonisjs/core/http').HttpContext) => boolean;
861
+ /**
862
+ * Escape hatch to register sensitive routes without an access guard.
863
+ * Exposes the dashboard without auth — local development only.
864
+ */
865
+ unsafeAllowNoAuth?: boolean;
866
+ /** Debug panel configuration (new name/shape replacing {@link devToolbar}). */
867
+ toolbar?: boolean | ToolbarConfig;
868
+ /** Full-page dashboard configuration. */
869
+ dashboard?: boolean | DashboardConfig;
870
+ /** Advanced fine-tuning options. */
871
+ advanced?: AdvancedConfig;
841
872
  /** Whether verbose informational logging is enabled. Always present after `defineConfig()`. */
842
873
  verbose: boolean;
843
874
  }
@@ -36,12 +36,16 @@ export async function appImport(specifier) {
36
36
  try {
37
37
  return await import(pathToFileURL(resolved).href);
38
38
  }
39
- catch { }
39
+ catch (err) {
40
+ errors.push(err);
41
+ }
40
42
  // Strategy B: CJS require (works for native addons like better-sqlite3)
41
43
  try {
42
44
  return appRequire(specifier);
43
45
  }
44
- catch { }
46
+ catch (err) {
47
+ errors.push(err);
48
+ }
45
49
  }
46
50
  catch (err) {
47
51
  errors.push(err);
@@ -70,13 +74,17 @@ export async function appImportWithPath(specifier) {
70
74
  const module = await import(pathToFileURL(resolved).href);
71
75
  return { module: module, resolvedPath: resolved };
72
76
  }
73
- catch { }
77
+ catch (err) {
78
+ errors.push(err);
79
+ }
74
80
  // Strategy B: CJS require (works for native addons like better-sqlite3)
75
81
  try {
76
82
  const module = appRequire(specifier);
77
83
  return { module, resolvedPath: resolved };
78
84
  }
79
- catch { }
85
+ catch (err) {
86
+ errors.push(err);
87
+ }
80
88
  }
81
89
  catch (err) {
82
90
  errors.push(err);