adonisjs-server-stats 1.12.3 → 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 (168) hide show
  1. package/README.md +11 -6
  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/adonisjs_queue_collector.d.ts +21 -0
  38. package/dist/src/collectors/adonisjs_queue_collector.js +61 -0
  39. package/dist/src/collectors/auto_detect.js +8 -2
  40. package/dist/src/collectors/index.d.ts +2 -0
  41. package/dist/src/collectors/index.js +1 -0
  42. package/dist/src/collectors/queue_collector.js +22 -6
  43. package/dist/src/config/deprecation_migration.js +11 -0
  44. package/dist/src/dashboard/cache_handlers.js +22 -2
  45. package/dist/src/dashboard/flush_manager.d.ts +2 -0
  46. package/dist/src/dashboard/flush_manager.js +17 -0
  47. package/dist/src/dashboard/inspector_manager.d.ts +4 -4
  48. package/dist/src/dashboard/inspector_manager.js +20 -10
  49. package/dist/src/dashboard/integrations/adonisjs_queue_inspector.d.ts +42 -0
  50. package/dist/src/dashboard/integrations/adonisjs_queue_inspector.js +135 -0
  51. package/dist/src/dashboard/integrations/adonisjs_queue_store.d.ts +166 -0
  52. package/dist/src/dashboard/integrations/adonisjs_queue_store.js +629 -0
  53. package/dist/src/dashboard/integrations/config_inspector.d.ts +0 -1
  54. package/dist/src/dashboard/integrations/config_inspector.js +4 -2
  55. package/dist/src/dashboard/integrations/index.d.ts +4 -1
  56. package/dist/src/dashboard/integrations/index.js +2 -0
  57. package/dist/src/dashboard/integrations/queue_inspector.d.ts +5 -61
  58. package/dist/src/dashboard/integrations/queue_inspector.js +5 -1
  59. package/dist/src/dashboard/integrations/queue_inspector_contract.d.ts +73 -0
  60. package/dist/src/dashboard/integrations/queue_inspector_contract.js +15 -0
  61. package/dist/src/dashboard/paginate_helper.d.ts +5 -0
  62. package/dist/src/dashboard/paginate_helper.js +16 -4
  63. package/dist/src/dashboard/query_explain_handler.d.ts +6 -1
  64. package/dist/src/dashboard/query_explain_handler.js +12 -2
  65. package/dist/src/dashboard/write_queue.d.ts +13 -0
  66. package/dist/src/dashboard/write_queue.js +34 -2
  67. package/dist/src/debug/debug_store.js +8 -1
  68. package/dist/src/debug/event_collector.js +4 -0
  69. package/dist/src/debug/query_collector.js +2 -0
  70. package/dist/src/debug/ring_buffer.js +8 -1
  71. package/dist/src/debug/trace_collector.js +34 -10
  72. package/dist/src/debug/types.d.ts +2 -0
  73. package/dist/src/define_config.js +1 -0
  74. package/dist/src/edge/client/dashboard.js +2 -2
  75. package/dist/src/edge/client/debug-panel-deferred.js +1 -1
  76. package/dist/src/edge/client/stats-bar.js +1 -1
  77. package/dist/src/edge/client-vue/dashboard.js +5 -5
  78. package/dist/src/edge/client-vue/debug-panel-deferred.js +2 -2
  79. package/dist/src/engine/request_metrics.js +8 -10
  80. package/dist/src/log_stream/log_stream_service.d.ts +1 -0
  81. package/dist/src/log_stream/log_stream_service.js +7 -5
  82. package/dist/src/prometheus/prometheus_collector.d.ts +9 -0
  83. package/dist/src/prometheus/prometheus_collector.js +11 -0
  84. package/dist/src/provider/dashboard_init.js +15 -0
  85. package/dist/src/provider/dashboard_setup.js +9 -2
  86. package/dist/src/provider/email_bridge.d.ts +14 -3
  87. package/dist/src/provider/email_bridge.js +25 -3
  88. package/dist/src/provider/email_helpers.d.ts +9 -1
  89. package/dist/src/provider/email_helpers.js +32 -4
  90. package/dist/src/provider/pino_hook.d.ts +8 -0
  91. package/dist/src/provider/pino_hook.js +19 -1
  92. package/dist/src/provider/provider_helpers_extra.d.ts +2 -0
  93. package/dist/src/provider/provider_helpers_extra.js +17 -2
  94. package/dist/src/provider/server_stats_provider.js +19 -1
  95. package/dist/src/provider/shutdown_helpers.js +2 -0
  96. package/dist/src/provider/toolbar_setup.js +32 -17
  97. package/dist/src/routes/dashboard_routes.js +96 -62
  98. package/dist/src/routes/debug_routes.js +45 -35
  99. package/dist/src/routes/no_session_middleware.d.ts +6 -3
  100. package/dist/src/routes/no_session_middleware.js +7 -3
  101. package/dist/src/routes/register_routes.d.ts +8 -0
  102. package/dist/src/routes/register_routes.js +19 -0
  103. package/dist/src/types.d.ts +31 -0
  104. package/dist/src/utils/app_import.js +12 -4
  105. package/dist/vue/CacheSection-CxEBVVkF.js +156 -0
  106. package/dist/vue/ConfigSection-BiRAiaHj.js +415 -0
  107. package/dist/vue/EmailsSection-Dl44qyqY.js +208 -0
  108. package/dist/vue/{EmailsTab-CwIF1fik.js → EmailsTab-Diya54CP.js} +6 -5
  109. package/dist/vue/{EventsSection-BpgkWIwM.js → EventsSection-CWjeitjU.js} +1 -1
  110. package/dist/vue/{InternalsTab-521fxYYj.js → InternalsTab-Z3c82glB.js} +15 -15
  111. package/dist/vue/{JobsSection-DrghFEKL.js → JobsSection-DOBb4LjZ.js} +1 -1
  112. package/dist/vue/LogsSection-CXx-HOWJ.js +260 -0
  113. package/dist/vue/{OverviewSection-BAgZTPjY.js → OverviewSection-CyfNQ8uV.js} +318 -302
  114. package/dist/vue/{QueriesSection-CUpwhp7u.js → QueriesSection-CXBsFp-y.js} +1 -1
  115. package/dist/vue/{RequestsSection-D8P2xpF2.js → RequestsSection-YTIaZGZd.js} +151 -145
  116. package/dist/vue/{RoutesSection-0qB81hTT.js → RoutesSection-CDKMey49.js} +1 -1
  117. package/dist/vue/composables/useApiClient.d.ts +8 -1
  118. package/dist/vue/index-14x39RI_.js +1239 -0
  119. package/dist/vue/index.js +1 -1
  120. package/dist/vue/style.css +1 -1
  121. package/package.json +16 -3
  122. package/dist/react/ConfigContent-CnsEI4j3.js +0 -397
  123. package/dist/react/LogsTab-jKwv9G7Q.js +0 -79
  124. package/dist/react/RequestsSection-D8cMbZU0.js +0 -321
  125. package/dist/react/index-CsprmgzI.js +0 -1121
  126. package/dist/react/useApiClient-BVtNCmnL.js +0 -9
  127. package/dist/vue/CacheSection-DRqV3YX2.js +0 -149
  128. package/dist/vue/ConfigSection-C6pQCHAL.js +0 -576
  129. package/dist/vue/EmailsSection-BTNw3ZU2.js +0 -206
  130. package/dist/vue/LogsSection-BDxx9Bfi.js +0 -258
  131. package/dist/vue/index-30jLw-_w.js +0 -1232
  132. /package/dist/core/{api-client.d.ts → core/api-client.d.ts} +0 -0
  133. /package/dist/core/{config-utils.d.ts → core/config-utils.d.ts} +0 -0
  134. /package/dist/core/{constants.d.ts → core/constants.d.ts} +0 -0
  135. /package/dist/core/{dashboard-api.d.ts → core/dashboard-api.d.ts} +0 -0
  136. /package/dist/core/{dashboard-data-controller.d.ts → core/dashboard-data-controller.d.ts} +0 -0
  137. /package/dist/core/{dashboard-data-helpers.d.ts → core/dashboard-data-helpers.d.ts} +0 -0
  138. /package/dist/core/{debug-data-controller.d.ts → core/debug-data-controller.d.ts} +0 -0
  139. /package/dist/core/{define-config-helpers.d.ts → core/define-config-helpers.d.ts} +0 -0
  140. /package/dist/core/{explain-utils.d.ts → core/explain-utils.d.ts} +0 -0
  141. /package/dist/core/{feature-detect-helpers.d.ts → core/feature-detect-helpers.d.ts} +0 -0
  142. /package/dist/core/{feature-detect.d.ts → core/feature-detect.d.ts} +0 -0
  143. /package/dist/core/{field-resolvers.d.ts → core/field-resolvers.d.ts} +0 -0
  144. /package/dist/core/{formatters-helpers.d.ts → core/formatters-helpers.d.ts} +0 -0
  145. /package/dist/core/{formatters.d.ts → core/formatters.d.ts} +0 -0
  146. /package/dist/core/{history-buffer.d.ts → core/history-buffer.d.ts} +0 -0
  147. /package/dist/core/{icons.d.ts → core/icons.d.ts} +0 -0
  148. /package/dist/core/{index.d.ts → core/index.d.ts} +0 -0
  149. /package/dist/core/{internals-utils.d.ts → core/internals-utils.d.ts} +0 -0
  150. /package/dist/core/{job-utils.d.ts → core/job-utils.d.ts} +0 -0
  151. /package/dist/core/{log-utils-helpers.d.ts → core/log-utils-helpers.d.ts} +0 -0
  152. /package/dist/core/{log-utils.d.ts → core/log-utils.d.ts} +0 -0
  153. /package/dist/core/{metrics.d.ts → core/metrics.d.ts} +0 -0
  154. /package/dist/core/{pagination.d.ts → core/pagination.d.ts} +0 -0
  155. /package/dist/core/{queries-columns.d.ts → core/queries-columns.d.ts} +0 -0
  156. /package/dist/core/{queries-controller.d.ts → core/queries-controller.d.ts} +0 -0
  157. /package/dist/core/{query-utils.d.ts → core/query-utils.d.ts} +0 -0
  158. /package/dist/core/{resizable-columns.d.ts → core/resizable-columns.d.ts} +0 -0
  159. /package/dist/core/{routes.d.ts → core/routes.d.ts} +0 -0
  160. /package/dist/core/{server-stats-controller.d.ts → core/server-stats-controller.d.ts} +0 -0
  161. /package/dist/core/{sparkline.d.ts → core/sparkline.d.ts} +0 -0
  162. /package/dist/core/{split-pane.d.ts → core/split-pane.d.ts} +0 -0
  163. /package/dist/core/{theme.d.ts → core/theme.d.ts} +0 -0
  164. /package/dist/core/{trace-utils.d.ts → core/trace-utils.d.ts} +0 -0
  165. /package/dist/core/{transmit-adapter.d.ts → core/transmit-adapter.d.ts} +0 -0
  166. /package/dist/core/{transmit-helpers.d.ts → core/transmit-helpers.d.ts} +0 -0
  167. /package/dist/core/{types-dashboard.d.ts → core/types-dashboard.d.ts} +0 -0
  168. /package/dist/core/{types-diagnostics.d.ts → core/types-diagnostics.d.ts} +0 -0
@@ -1,4 +1,8 @@
1
1
  import type { ApplicationService } from '@adonisjs/core/types';
2
+ import type { QueueInspectorContract } from './queue_inspector_contract.js';
3
+ export type { QueueOverview, QueueJobSummary, QueueJobDetail, QueueJobListResult, JobStatus, } from './queue_inspector_contract.js';
4
+ export { ALL_STATUSES } from './queue_inspector_contract.js';
5
+ import type { QueueOverview, QueueJobDetail, QueueJobListResult, JobStatus } from './queue_inspector_contract.js';
2
6
  /** Minimal interface for a BullMQ Job as accessed by the inspector. */
3
7
  interface BullMQJob {
4
8
  id: string | number;
@@ -29,65 +33,6 @@ interface QueueManager {
29
33
  getOrSet?(name: string): BullMQQueue;
30
34
  getJobCounts?(...statuses: string[]): Promise<Record<string, number>>;
31
35
  }
32
- export interface QueueOverview {
33
- /** Total jobs currently being processed. */
34
- active: number;
35
- /** Jobs waiting to be picked up by a worker. */
36
- waiting: number;
37
- /** Jobs scheduled for future execution. */
38
- delayed: number;
39
- /** Jobs that completed successfully. */
40
- completed: number;
41
- /** Jobs that permanently failed. */
42
- failed: number;
43
- /** Jobs paused in the queue. */
44
- paused: number;
45
- }
46
- export interface QueueJobSummary {
47
- /** Bull job ID. */
48
- id: string;
49
- /** Human-readable job name (cleaned from file URLs). */
50
- name: string;
51
- /** Current job status. */
52
- status: 'active' | 'waiting' | 'delayed' | 'completed' | 'failed' | 'paused';
53
- /** Job payload (data). */
54
- data: Record<string, unknown> | null;
55
- /** Alias for `data` — used by some frontends. */
56
- payload: Record<string, unknown> | null;
57
- /** Number of attempts so far. */
58
- attempts: number;
59
- /** Maximum allowed attempts. */
60
- maxAttempts: number;
61
- /** Job progress (0-100 or custom). */
62
- progress: number | object;
63
- /** Error message if the job failed, or null. */
64
- failedReason: string | null;
65
- /** When the job was added (Unix timestamp ms). */
66
- createdAt: number;
67
- /** Alias for `createdAt` — BullMQ compat. */
68
- timestamp: number;
69
- /** When processing started (Unix timestamp ms), or null. */
70
- processedAt: number | null;
71
- /** When the job finished (Unix timestamp ms), or null. */
72
- finishedAt: number | null;
73
- /** Processing duration in ms, or null if not finished. */
74
- duration: number | null;
75
- }
76
- export interface QueueJobDetail extends QueueJobSummary {
77
- /** Full stack trace if the job failed. */
78
- stackTrace: string[];
79
- /** Return value from the job handler, if any. */
80
- returnValue: unknown;
81
- /** Job options (delay, priority, repeat, etc.). */
82
- opts: Record<string, unknown>;
83
- }
84
- export interface QueueJobListResult {
85
- /** Jobs for the requested page. */
86
- jobs: QueueJobSummary[];
87
- /** Total number of jobs matching the status filter. */
88
- total: number;
89
- }
90
- type JobStatus = 'active' | 'waiting' | 'delayed' | 'completed' | 'failed' | 'paused';
91
36
  /**
92
37
  * Inspects Bull Queue jobs, counts, and allows retrying failed jobs.
93
38
  *
@@ -95,7 +40,7 @@ type JobStatus = 'active' | 'waiting' | 'delayed' | 'completed' | 'failed' | 'pa
95
40
  * Only functional when `@rlanz/bull-queue` is installed.
96
41
  * All methods catch errors and return safe defaults.
97
42
  */
98
- export declare class QueueInspector {
43
+ export declare class QueueInspector implements QueueInspectorContract {
99
44
  private queueManager;
100
45
  constructor(queueManager: QueueManager);
101
46
  /**
@@ -147,4 +92,3 @@ export declare class QueueInspector {
147
92
  /** Format a Bull job into our summary shape. */
148
93
  private formatJobSummary;
149
94
  }
150
- export {};
@@ -1,4 +1,8 @@
1
- const ALL_STATUSES = ['active', 'waiting', 'delayed', 'completed', 'failed', 'paused'];
1
+ export { ALL_STATUSES } from './queue_inspector_contract.js';
2
+ import { ALL_STATUSES } from './queue_inspector_contract.js';
3
+ // ---------------------------------------------------------------------------
4
+ // QueueInspector
5
+ // ---------------------------------------------------------------------------
2
6
  /**
3
7
  * Inspects Bull Queue jobs, counts, and allows retrying failed jobs.
4
8
  *
@@ -0,0 +1,73 @@
1
+ export interface QueueOverview {
2
+ /** Total jobs currently being processed. */
3
+ active: number;
4
+ /** Jobs waiting to be picked up by a worker. */
5
+ waiting: number;
6
+ /** Jobs scheduled for future execution. */
7
+ delayed: number;
8
+ /** Jobs that completed successfully. */
9
+ completed: number;
10
+ /** Jobs that permanently failed. */
11
+ failed: number;
12
+ /** Jobs paused in the queue. */
13
+ paused: number;
14
+ }
15
+ export interface QueueJobSummary {
16
+ /** Bull / @boringnode job ID. */
17
+ id: string;
18
+ /** Human-readable job name (cleaned from file URLs). */
19
+ name: string;
20
+ /** Current job status. */
21
+ status: 'active' | 'waiting' | 'delayed' | 'completed' | 'failed' | 'paused';
22
+ /** Job payload (data). */
23
+ data: Record<string, unknown> | null;
24
+ /** Alias for `data` — used by some frontends. */
25
+ payload: Record<string, unknown> | null;
26
+ /** Number of attempts so far. */
27
+ attempts: number;
28
+ /** Maximum allowed attempts. */
29
+ maxAttempts: number;
30
+ /** Job progress (0-100 or custom). */
31
+ progress: number | object;
32
+ /** Error message if the job failed, or null. */
33
+ failedReason: string | null;
34
+ /** When the job was added (Unix timestamp ms). */
35
+ createdAt: number;
36
+ /** Alias for `createdAt` — BullMQ compat. */
37
+ timestamp: number;
38
+ /** When processing started (Unix timestamp ms), or null. */
39
+ processedAt: number | null;
40
+ /** When the job finished (Unix timestamp ms), or null. */
41
+ finishedAt: number | null;
42
+ /** Processing duration in ms, or null if not finished. */
43
+ duration: number | null;
44
+ }
45
+ export interface QueueJobDetail extends QueueJobSummary {
46
+ /** Full stack trace if the job failed. */
47
+ stackTrace: string[];
48
+ /** Return value from the job handler, if any. */
49
+ returnValue: unknown;
50
+ /** Job options (delay, priority, repeat, etc.). */
51
+ opts: Record<string, unknown>;
52
+ }
53
+ export interface QueueJobListResult {
54
+ /** Jobs for the requested page. */
55
+ jobs: QueueJobSummary[];
56
+ /** Total number of jobs matching the status filter. */
57
+ total: number;
58
+ }
59
+ /** The statuses understood by the dashboard UI. */
60
+ export type JobStatus = 'active' | 'waiting' | 'delayed' | 'completed' | 'failed' | 'paused';
61
+ /** All dashboard-level job statuses. */
62
+ export declare const ALL_STATUSES: JobStatus[];
63
+ /**
64
+ * Common interface implemented by both BullMQ and @adonisjs/queue inspectors.
65
+ *
66
+ * All methods catch errors internally and return safe defaults.
67
+ */
68
+ export interface QueueInspectorContract {
69
+ getOverview(): Promise<QueueOverview>;
70
+ listJobs(status: JobStatus | 'all', page?: number, perPage?: number): Promise<QueueJobListResult>;
71
+ getJob(id: string): Promise<QueueJobDetail | null>;
72
+ retryJob(id: string): Promise<boolean>;
73
+ }
@@ -0,0 +1,15 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Shared response types and contract interface for queue inspection.
3
+ //
4
+ // Both QueueInspector (BullMQ / @rlanz/bull-queue) and
5
+ // AdonisQueueInspector (@adonisjs/queue) implement QueueInspectorContract.
6
+ // ---------------------------------------------------------------------------
7
+ /** All dashboard-level job statuses. */
8
+ export const ALL_STATUSES = [
9
+ 'active',
10
+ 'waiting',
11
+ 'delayed',
12
+ 'completed',
13
+ 'failed',
14
+ 'paused',
15
+ ];
@@ -6,6 +6,11 @@
6
6
  */
7
7
  import type { PaginatedResult, PaginateOptions } from './dashboard_types.js';
8
8
  import type { Knex } from 'knex';
9
+ /**
10
+ * Clamp a raw perPage value to a safe range (1..200) to prevent DoS via
11
+ * unbounded result sets on the single sqlite connection.
12
+ */
13
+ export declare function clampPerPage(value: number): number;
9
14
  /**
10
15
  * Execute a paginated query within a transaction.
11
16
  */
@@ -4,20 +4,32 @@
4
4
  * Wraps COUNT + SELECT in a single transaction so the pool connection
5
5
  * is acquired only once instead of two separate acquire/release cycles.
6
6
  */
7
+ import { clamp } from '../utils/math_helpers.js';
8
+ /**
9
+ * Clamp a raw perPage value to a safe range (1..200) to prevent DoS via
10
+ * unbounded result sets on the single sqlite connection.
11
+ */
12
+ export function clampPerPage(value) {
13
+ const n = Number(value);
14
+ if (!Number.isFinite(n))
15
+ return 25;
16
+ return clamp(Math.trunc(n), 1, 200);
17
+ }
7
18
  /**
8
19
  * Execute a paginated query within a transaction.
9
20
  */
10
21
  export async function executePaginate(db, opts) {
22
+ const perPage = clampPerPage(opts.perPage);
11
23
  return db.transaction(async (trx) => {
12
24
  const countQuery = trx(opts.table);
13
25
  if (opts.applyFilters)
14
26
  opts.applyFilters(countQuery);
15
27
  const [{ count: totalRaw }] = await countQuery.count('* as count');
16
28
  const total = Number(totalRaw);
17
- const offset = (opts.page - 1) * opts.perPage;
29
+ const offset = (opts.page - 1) * perPage;
18
30
  const dataQuery = trx(opts.table)
19
31
  .orderBy('created_at', 'desc')
20
- .limit(opts.perPage)
32
+ .limit(perPage)
21
33
  .offset(offset);
22
34
  if (opts.applyFilters)
23
35
  opts.applyFilters(dataQuery);
@@ -26,8 +38,8 @@ export async function executePaginate(db, opts) {
26
38
  data,
27
39
  total,
28
40
  page: opts.page,
29
- perPage: opts.perPage,
30
- lastPage: Math.ceil(total / opts.perPage),
41
+ perPage,
42
+ lastPage: Math.ceil(total / perPage),
31
43
  };
32
44
  });
33
45
  }
@@ -1,12 +1,17 @@
1
1
  import type { DashboardStore } from './dashboard_store.js';
2
2
  import type { HttpContext } from '@adonisjs/core/http';
3
3
  import type { ApplicationService } from '@adonisjs/core/types';
4
+ /**
5
+ * Reject SQL containing statement separators / comment sequences that could
6
+ * enable stacked statements or comment-based injection into the EXPLAIN prefix.
7
+ */
8
+ export declare function hasUnsafeSqlTokens(sqlText: string): boolean;
4
9
  export type DbDialect = 'pg' | 'sqlite' | 'mysql' | 'mssql' | 'unknown';
5
10
  export interface AppDbClient {
6
11
  raw: (sql: string, bindings: unknown[]) => Promise<unknown>;
7
12
  dialect: DbDialect;
8
13
  }
9
- /** Get the Lucid write client for running EXPLAIN. */
14
+ /** Get the Lucid read client for running EXPLAIN. */
10
15
  export declare function getAppDbClient(app: ApplicationService, connectionName?: string): Promise<AppDbClient | null>;
11
16
  /** Build the EXPLAIN SQL for the detected dialect. */
12
17
  export declare function buildExplainSql(sql: string, dialect: DbDialect): string;
@@ -12,6 +12,13 @@ async function fetchQueryRecord(dashboardStore, id) {
12
12
  function isSelectQuery(sqlText) {
13
13
  return sqlText.trim().toUpperCase().startsWith('SELECT');
14
14
  }
15
+ /**
16
+ * Reject SQL containing statement separators / comment sequences that could
17
+ * enable stacked statements or comment-based injection into the EXPLAIN prefix.
18
+ */
19
+ export function hasUnsafeSqlTokens(sqlText) {
20
+ return /;|--|\/\*/.test(sqlText);
21
+ }
15
22
  /** Parse bindings from a stored query. */
16
23
  function parseBindings(raw) {
17
24
  if (!raw)
@@ -39,14 +46,14 @@ function detectDialect(client) {
39
46
  return 'mssql';
40
47
  return 'unknown';
41
48
  }
42
- /** Get the Lucid write client for running EXPLAIN. */
49
+ /** Get the Lucid read client for running EXPLAIN. */
43
50
  export async function getAppDbClient(app, connectionName) {
44
51
  try {
45
52
  const lucid = await app.container.make('lucid.db');
46
53
  const conn = connectionName
47
54
  ? lucid.connection(connectionName)
48
55
  : lucid.connection();
49
- const client = conn.getWriteClient();
56
+ const client = conn.getReadClient();
50
57
  const dialect = detectDialect(client);
51
58
  return {
52
59
  raw: client.raw.bind(client),
@@ -111,6 +118,9 @@ export async function handleQueryExplain(dashboardStore, app, ctx) {
111
118
  if (!isSelectQuery(query.sql_text)) {
112
119
  return response.badRequest({ error: 'EXPLAIN is only supported for SELECT queries' });
113
120
  }
121
+ if (hasUnsafeSqlTokens(query.sql_text)) {
122
+ return response.badRequest({ error: 'EXPLAIN rejected: query contains disallowed tokens' });
123
+ }
114
124
  const appDb = await getAppDbClient(app, query.connection || undefined);
115
125
  if (!appDb) {
116
126
  return response.serviceUnavailable({ error: 'App database connection not available' });
@@ -13,8 +13,21 @@ export declare function markWarned(path: string): void;
13
13
  /**
14
14
  * Normalize a SQL query by replacing literal values with `?` placeholders.
15
15
  * Used for grouping identical query patterns.
16
+ *
17
+ * Numeric replacement is restricted to value contexts (a digit run preceded
18
+ * by whitespace, a comma, a paren, or an operator) so identifiers that contain
19
+ * digits — e.g. `orders_2024` — are left intact and distinct queries are not
20
+ * merged together.
16
21
  */
17
22
  export declare function normalizeSql(sql: string): string;
23
+ /**
24
+ * Redact/truncate SQL bindings before persistence so secret-looking values
25
+ * (long tokens, hashes, keys) are not stored in cleartext.
26
+ *
27
+ * Conservative: only long strings are truncated; short values (ids, flags,
28
+ * emails, ordinary params) pass through so normal capture is unaffected.
29
+ */
30
+ export declare function sanitizeBindings(bindings: unknown): unknown;
18
31
  export interface PreparedQuery {
19
32
  sql_text: string;
20
33
  sql_normalized: string;
@@ -22,15 +22,47 @@ export function markWarned(path) {
22
22
  /**
23
23
  * Normalize a SQL query by replacing literal values with `?` placeholders.
24
24
  * Used for grouping identical query patterns.
25
+ *
26
+ * Numeric replacement is restricted to value contexts (a digit run preceded
27
+ * by whitespace, a comma, a paren, or an operator) so identifiers that contain
28
+ * digits — e.g. `orders_2024` — are left intact and distinct queries are not
29
+ * merged together.
25
30
  */
26
31
  export function normalizeSql(sql) {
27
32
  return sql
28
33
  .replace(/'[^']*'/g, '?')
29
- .replace(/\b\d+(\.\d+)?\b/g, '?')
34
+ .replace(/(^|[\s,(=<>+\-*/])\d+(\.\d+)?\b/g, '$1?')
30
35
  .replace(/\s+/g, ' ')
31
36
  .trim();
32
37
  }
33
38
  // ---------------------------------------------------------------------------
39
+ // Binding hygiene
40
+ // ---------------------------------------------------------------------------
41
+ /** Max length for a persisted string binding before it is truncated. */
42
+ const MAX_BINDING_LEN = 256;
43
+ /**
44
+ * Redact/truncate SQL bindings before persistence so secret-looking values
45
+ * (long tokens, hashes, keys) are not stored in cleartext.
46
+ *
47
+ * Conservative: only long strings are truncated; short values (ids, flags,
48
+ * emails, ordinary params) pass through so normal capture is unaffected.
49
+ */
50
+ export function sanitizeBindings(bindings) {
51
+ if (Array.isArray(bindings))
52
+ return bindings.map(sanitizeBindings);
53
+ if (typeof bindings === 'string' && bindings.length > MAX_BINDING_LEN) {
54
+ return bindings.slice(0, MAX_BINDING_LEN) + `…[truncated ${bindings.length} chars]`;
55
+ }
56
+ if (bindings && typeof bindings === 'object') {
57
+ const out = {};
58
+ for (const [k, v] of Object.entries(bindings)) {
59
+ out[k] = sanitizeBindings(v);
60
+ }
61
+ return out;
62
+ }
63
+ return bindings;
64
+ }
65
+ // ---------------------------------------------------------------------------
34
66
  // Pure data-prep functions
35
67
  // ---------------------------------------------------------------------------
36
68
  /**
@@ -46,7 +78,7 @@ export function prepareRequestRows(requests) {
46
78
  .map((q) => ({
47
79
  sql_text: q.sql,
48
80
  sql_normalized: normalizeSql(q.sql),
49
- bindings: q.bindings ? JSON.stringify(q.bindings) : null,
81
+ bindings: q.bindings ? JSON.stringify(sanitizeBindings(q.bindings)) : null,
50
82
  duration: round(q.duration),
51
83
  method: q.method,
52
84
  model: q.model,
@@ -1,5 +1,6 @@
1
1
  import { writeFile, readFile, rename, mkdir } from 'node:fs/promises';
2
2
  import { dirname } from 'node:path';
3
+ import { redactTokenLinks } from '../provider/email_helpers.js';
3
4
  import { log, bold } from '../utils/logger.js';
4
5
  import { EmailCollector } from './email_collector.js';
5
6
  import { EventCollector } from './event_collector.js';
@@ -103,7 +104,13 @@ export class DebugStore {
103
104
  const events = this.events.getEvents();
104
105
  parts.push(`"events":${JSON.stringify(events)},`);
105
106
  await new Promise((resolve) => setImmediate(resolve));
106
- const emails = this.emails.getEmails();
107
+ // Strip token-bearing links (reset/verify/magic URLs, ?token=…) from email
108
+ // bodies before they hit disk in cleartext.
109
+ const emails = this.emails.getEmails().map((email) => ({
110
+ ...email,
111
+ html: typeof email.html === 'string' ? redactTokenLinks(email.html) : email.html,
112
+ text: typeof email.text === 'string' ? redactTokenLinks(email.text) : email.text,
113
+ }));
107
114
  parts.push(`"emails":${JSON.stringify(emails)}`);
108
115
  if (this.traces) {
109
116
  await new Promise((resolve) => setImmediate(resolve));
@@ -87,6 +87,10 @@ export class EventCollector {
87
87
  start(emitter) {
88
88
  if (!emitter || typeof emitter.emit !== 'function')
89
89
  return;
90
+ // Idempotent: a second start() must not capture the already-patched emit as
91
+ // the "original", which would corrupt emitter.emit permanently.
92
+ if (this.originalEmit)
93
+ return;
90
94
  this.emitter = emitter;
91
95
  this.originalEmit = emitter.emit.bind(emitter);
92
96
  emitter.emit = (event, data) => {
@@ -122,6 +122,8 @@ export class QueryCollector {
122
122
  }
123
123
  clear() {
124
124
  this.buffer.clear();
125
+ // Invalidate the cached summary so totals aren't stale after a clear().
126
+ this.cachedSummary = null;
125
127
  }
126
128
  /** Register a callback that fires whenever a new query is recorded. */
127
129
  onNewItem(cb) {
@@ -23,7 +23,14 @@ export class RingBuffer {
23
23
  if (this.count < this.capacity) {
24
24
  this.count++;
25
25
  }
26
- this.pushCallback?.(item);
26
+ // The write itself is infallible; a throwing subscriber must not escape and
27
+ // corrupt callers that rely on push() never failing.
28
+ try {
29
+ this.pushCallback?.(item);
30
+ }
31
+ catch (err) {
32
+ console.error('[server-stats] ring buffer push callback threw:', err);
33
+ }
27
34
  }
28
35
  /** Returns all items in insertion order (oldest first). */
29
36
  toArray() {
@@ -6,6 +6,12 @@ import { RingBuffer } from './ring_buffer.js';
6
6
  * Module-level singleton reference for the `trace()` helper.
7
7
  */
8
8
  const globalRef = { current: null };
9
+ /**
10
+ * Tracks whether console.warn is currently patched by a TraceCollector, so a
11
+ * second instance (or a skipped stop()) cannot double-wrap it and leak the
12
+ * patch permanently.
13
+ */
14
+ const consoleWarnPatch = { active: false };
9
15
  /**
10
16
  * Wrap an async function in a traced span.
11
17
  *
@@ -101,9 +107,14 @@ export class TraceCollector {
101
107
  const parentId = ctx.currentSpanId;
102
108
  const spanId = String(ctx.nextSpanId++);
103
109
  ctx.currentSpanId = spanId;
110
+ let errored = false;
104
111
  try {
105
112
  return await fn();
106
113
  }
114
+ catch (err) {
115
+ errored = true;
116
+ throw err;
117
+ }
107
118
  finally {
108
119
  const duration = performance.now() - start;
109
120
  ctx.spans.push({
@@ -113,6 +124,7 @@ export class TraceCollector {
113
124
  category,
114
125
  startOffset: round(start - ctx.requestStart),
115
126
  duration: round(duration),
127
+ error: errored ? true : undefined,
116
128
  });
117
129
  ctx.currentSpanId = parentId;
118
130
  }
@@ -145,28 +157,40 @@ export class TraceCollector {
145
157
  };
146
158
  emitter.on('db:query', this.dbHandler);
147
159
  }
148
- // Intercept console.warn to capture warnings per-request
149
- this.originalConsoleWarn = console.warn;
150
- console.warn = (...args) => {
151
- const ctx = this.als.getStore();
152
- if (ctx) {
153
- ctx.warnings.push(args.map(String).join(' '));
154
- }
155
- this.originalConsoleWarn.apply(console, args);
156
- };
160
+ // Intercept console.warn to capture warnings per-request. Guard against
161
+ // double-wrapping: if another instance (or a prior start() without a
162
+ // matching stop()) already patched console.warn, don't wrap it again —
163
+ // that would capture the patched fn as the "original" and leak the patch.
164
+ if (!consoleWarnPatch.active) {
165
+ consoleWarnPatch.active = true;
166
+ this.originalConsoleWarn = console.warn;
167
+ console.warn = (...args) => {
168
+ const ctx = this.als.getStore();
169
+ if (ctx) {
170
+ ctx.warnings.push(args.map(String).join(' '));
171
+ }
172
+ this.originalConsoleWarn.apply(console, args);
173
+ };
174
+ }
157
175
  }
158
176
  /** Unhook event listeners and restore console.warn. */
159
177
  stop() {
160
178
  if (this.emitter && this.dbHandler) {
161
179
  this.emitter.off('db:query', this.dbHandler);
162
180
  }
181
+ // Only restore console.warn if this instance was the one that patched it.
163
182
  if (this.originalConsoleWarn) {
164
183
  console.warn = this.originalConsoleWarn;
184
+ consoleWarnPatch.active = false;
165
185
  }
166
186
  this.dbHandler = null;
167
187
  this.emitter = null;
168
188
  this.originalConsoleWarn = null;
169
- globalRef.current = null;
189
+ // Only relinquish the global reference if it still points at this instance,
190
+ // so stopping an older instance can't clear a newer one's registration.
191
+ if (globalRef.current === this) {
192
+ globalRef.current = null;
193
+ }
170
194
  }
171
195
  getTraces() {
172
196
  return this.buffer.toArray().reverse();
@@ -220,6 +220,8 @@ export interface TraceSpan {
220
220
  startOffset: number;
221
221
  /** Span duration in milliseconds. */
222
222
  duration: number;
223
+ /** True when the wrapped function threw. */
224
+ error?: boolean;
223
225
  /** Optional metadata (query bindings, status code, etc.). */
224
226
  metadata?: Record<string, unknown>;
225
227
  }
@@ -140,6 +140,7 @@ export function defineConfig(config) {
140
140
  onStats: config.onStats,
141
141
  devToolbar: resolveDevToolbar(config),
142
142
  shouldShow: config.authorize ?? config.shouldShow,
143
+ unsafeAllowNoAuth: config.unsafeAllowNoAuth,
143
144
  verbose,
144
145
  };
145
146
  }