htpx-cli 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +665 -21
- package/README.md +422 -116
- package/dist/cli/commands/clear.d.ts.map +1 -1
- package/dist/cli/commands/clear.js +11 -11
- package/dist/cli/commands/clear.js.map +1 -1
- package/dist/cli/commands/daemon.d.ts +3 -0
- package/dist/cli/commands/daemon.d.ts.map +1 -0
- package/dist/cli/commands/daemon.js +59 -0
- package/dist/cli/commands/daemon.js.map +1 -0
- package/dist/cli/commands/debug-dump.d.ts.map +1 -1
- package/dist/cli/commands/debug-dump.js +8 -10
- package/dist/cli/commands/debug-dump.js.map +1 -1
- package/dist/cli/commands/helpers.d.ts +18 -0
- package/dist/cli/commands/helpers.d.ts.map +1 -0
- package/dist/cli/commands/helpers.js +34 -0
- package/dist/cli/commands/helpers.js.map +1 -0
- package/dist/cli/commands/init.d.ts +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +3 -4
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/intercept.d.ts +2 -1
- package/dist/cli/commands/intercept.d.ts.map +1 -1
- package/dist/cli/commands/intercept.js +74 -30
- package/dist/cli/commands/intercept.js.map +1 -1
- package/dist/cli/commands/interceptors.d.ts +3 -0
- package/dist/cli/commands/interceptors.d.ts.map +1 -0
- package/dist/cli/commands/interceptors.js +163 -0
- package/dist/cli/commands/interceptors.js.map +1 -0
- package/dist/cli/commands/mcp.d.ts +3 -0
- package/dist/cli/commands/mcp.d.ts.map +1 -0
- package/dist/cli/commands/mcp.js +24 -0
- package/dist/cli/commands/mcp.js.map +1 -0
- package/dist/cli/commands/off.d.ts +8 -0
- package/dist/cli/commands/off.d.ts.map +1 -0
- package/dist/cli/commands/off.js +34 -0
- package/dist/cli/commands/off.js.map +1 -0
- package/dist/cli/commands/on.d.ts +9 -0
- package/dist/cli/commands/on.d.ts.map +1 -0
- package/dist/cli/commands/on.js +121 -0
- package/dist/cli/commands/on.js.map +1 -0
- package/dist/cli/commands/project.d.ts.map +1 -1
- package/dist/cli/commands/project.js +5 -3
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/cli/commands/restart.d.ts.map +1 -1
- package/dist/cli/commands/restart.js +5 -10
- package/dist/cli/commands/restart.js.map +1 -1
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +50 -20
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/stop.d.ts.map +1 -1
- package/dist/cli/commands/stop.js +7 -10
- package/dist/cli/commands/stop.js.map +1 -1
- package/dist/cli/commands/tui.d.ts.map +1 -1
- package/dist/cli/commands/tui.js +6 -13
- package/dist/cli/commands/tui.js.map +1 -1
- package/dist/cli/index.js +12 -7
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/tui/App.d.ts +7 -2
- package/dist/cli/tui/App.d.ts.map +1 -1
- package/dist/cli/tui/App.js +490 -33
- package/dist/cli/tui/App.js.map +1 -1
- package/dist/cli/tui/components/AccordionContent.d.ts +28 -0
- package/dist/cli/tui/components/AccordionContent.d.ts.map +1 -0
- package/dist/cli/tui/components/AccordionContent.js +87 -0
- package/dist/cli/tui/components/AccordionContent.js.map +1 -0
- package/dist/cli/tui/components/AccordionPanel.d.ts +38 -0
- package/dist/cli/tui/components/AccordionPanel.d.ts.map +1 -0
- package/dist/cli/tui/components/AccordionPanel.js +110 -0
- package/dist/cli/tui/components/AccordionPanel.js.map +1 -0
- package/dist/cli/tui/components/AccordionSection.d.ts +32 -0
- package/dist/cli/tui/components/AccordionSection.d.ts.map +1 -0
- package/dist/cli/tui/components/AccordionSection.js +41 -0
- package/dist/cli/tui/components/AccordionSection.js.map +1 -0
- package/dist/cli/tui/components/ExportModal.d.ts +34 -0
- package/dist/cli/tui/components/ExportModal.d.ts.map +1 -0
- package/dist/cli/tui/components/ExportModal.js +109 -0
- package/dist/cli/tui/components/ExportModal.js.map +1 -0
- package/dist/cli/tui/components/FilterBar.d.ts +21 -0
- package/dist/cli/tui/components/FilterBar.d.ts.map +1 -0
- package/dist/cli/tui/components/FilterBar.js +155 -0
- package/dist/cli/tui/components/FilterBar.js.map +1 -0
- package/dist/cli/tui/components/HelpModal.d.ts +13 -0
- package/dist/cli/tui/components/HelpModal.d.ts.map +1 -0
- package/dist/cli/tui/components/HelpModal.js +78 -0
- package/dist/cli/tui/components/HelpModal.js.map +1 -0
- package/dist/cli/tui/components/HintContent.d.ts +25 -0
- package/dist/cli/tui/components/HintContent.d.ts.map +1 -0
- package/dist/cli/tui/components/HintContent.js +44 -0
- package/dist/cli/tui/components/HintContent.js.map +1 -0
- package/dist/cli/tui/components/InfoModal.d.ts +15 -0
- package/dist/cli/tui/components/InfoModal.d.ts.map +1 -0
- package/dist/cli/tui/components/InfoModal.js +17 -0
- package/dist/cli/tui/components/InfoModal.js.map +1 -0
- package/dist/cli/tui/components/JsonExplorerModal.d.ts +24 -0
- package/dist/cli/tui/components/JsonExplorerModal.d.ts.map +1 -0
- package/dist/cli/tui/components/JsonExplorerModal.js +311 -0
- package/dist/cli/tui/components/JsonExplorerModal.js.map +1 -0
- package/dist/cli/tui/components/Modal.d.ts +26 -0
- package/dist/cli/tui/components/Modal.d.ts.map +1 -0
- package/dist/cli/tui/components/Modal.js +15 -0
- package/dist/cli/tui/components/Modal.js.map +1 -0
- package/dist/cli/tui/components/Panel.d.ts +19 -0
- package/dist/cli/tui/components/Panel.d.ts.map +1 -0
- package/dist/cli/tui/components/Panel.js +37 -0
- package/dist/cli/tui/components/Panel.js.map +1 -0
- package/dist/cli/tui/components/RequestDetails.d.ts +4 -1
- package/dist/cli/tui/components/RequestDetails.d.ts.map +1 -1
- package/dist/cli/tui/components/RequestDetails.js +9 -5
- package/dist/cli/tui/components/RequestDetails.js.map +1 -1
- package/dist/cli/tui/components/RequestList.d.ts +9 -3
- package/dist/cli/tui/components/RequestList.d.ts.map +1 -1
- package/dist/cli/tui/components/RequestList.js +24 -11
- package/dist/cli/tui/components/RequestList.js.map +1 -1
- package/dist/cli/tui/components/RequestListItem.d.ts +26 -3
- package/dist/cli/tui/components/RequestListItem.d.ts.map +1 -1
- package/dist/cli/tui/components/RequestListItem.js +86 -9
- package/dist/cli/tui/components/RequestListItem.js.map +1 -1
- package/dist/cli/tui/components/SaveModal.d.ts +30 -0
- package/dist/cli/tui/components/SaveModal.d.ts.map +1 -0
- package/dist/cli/tui/components/SaveModal.js +95 -0
- package/dist/cli/tui/components/SaveModal.js.map +1 -0
- package/dist/cli/tui/components/StatusBar.d.ts +31 -2
- package/dist/cli/tui/components/StatusBar.d.ts.map +1 -1
- package/dist/cli/tui/components/StatusBar.js +44 -9
- package/dist/cli/tui/components/StatusBar.js.map +1 -1
- package/dist/cli/tui/components/TextViewerModal.d.ts +19 -0
- package/dist/cli/tui/components/TextViewerModal.d.ts.map +1 -0
- package/dist/cli/tui/components/TextViewerModal.js +227 -0
- package/dist/cli/tui/components/TextViewerModal.js.map +1 -0
- package/dist/cli/tui/hooks/useBodyExport.d.ts +26 -0
- package/dist/cli/tui/hooks/useBodyExport.d.ts.map +1 -0
- package/dist/cli/tui/hooks/useBodyExport.js +173 -0
- package/dist/cli/tui/hooks/useBodyExport.js.map +1 -0
- package/dist/cli/tui/hooks/useExport.d.ts +13 -2
- package/dist/cli/tui/hooks/useExport.d.ts.map +1 -1
- package/dist/cli/tui/hooks/useExport.js +46 -40
- package/dist/cli/tui/hooks/useExport.js.map +1 -1
- package/dist/cli/tui/hooks/useRequests.d.ts +9 -3
- package/dist/cli/tui/hooks/useRequests.d.ts.map +1 -1
- package/dist/cli/tui/hooks/useRequests.js +61 -15
- package/dist/cli/tui/hooks/useRequests.js.map +1 -1
- package/dist/cli/tui/hooks/useSaveBinary.d.ts +26 -0
- package/dist/cli/tui/hooks/useSaveBinary.d.ts.map +1 -0
- package/dist/cli/tui/hooks/useSaveBinary.js +165 -0
- package/dist/cli/tui/hooks/useSaveBinary.js.map +1 -0
- package/dist/cli/tui/hooks/useSpinner.d.ts +5 -0
- package/dist/cli/tui/hooks/useSpinner.d.ts.map +1 -0
- package/dist/cli/tui/hooks/useSpinner.js +25 -0
- package/dist/cli/tui/hooks/useSpinner.js.map +1 -0
- package/dist/cli/tui/utils/binary.d.ts +24 -0
- package/dist/cli/tui/utils/binary.d.ts.map +1 -0
- package/dist/cli/tui/utils/binary.js +152 -0
- package/dist/cli/tui/utils/binary.js.map +1 -0
- package/dist/cli/tui/utils/clipboard.d.ts +9 -0
- package/dist/cli/tui/utils/clipboard.d.ts.map +1 -0
- package/dist/cli/tui/utils/clipboard.js +58 -0
- package/dist/cli/tui/utils/clipboard.js.map +1 -0
- package/dist/cli/tui/utils/content-type.d.ts +8 -0
- package/dist/cli/tui/utils/content-type.d.ts.map +1 -0
- package/dist/cli/tui/utils/content-type.js +10 -0
- package/dist/cli/tui/utils/content-type.js.map +1 -0
- package/dist/cli/tui/utils/curl.d.ts.map +1 -1
- package/dist/cli/tui/utils/curl.js +9 -2
- package/dist/cli/tui/utils/curl.js.map +1 -1
- package/dist/cli/tui/utils/filters.d.ts +6 -0
- package/dist/cli/tui/utils/filters.d.ts.map +1 -0
- package/dist/cli/tui/utils/filters.js +13 -0
- package/dist/cli/tui/utils/filters.js.map +1 -0
- package/dist/cli/tui/utils/formatters.d.ts +8 -0
- package/dist/cli/tui/utils/formatters.d.ts.map +1 -1
- package/dist/cli/tui/utils/formatters.js +85 -0
- package/dist/cli/tui/utils/formatters.js.map +1 -1
- package/dist/cli/tui/utils/har.d.ts.map +1 -1
- package/dist/cli/tui/utils/har.js +3 -25
- package/dist/cli/tui/utils/har.js.map +1 -1
- package/dist/cli/tui/utils/json-tree.d.ts +69 -0
- package/dist/cli/tui/utils/json-tree.d.ts.map +1 -0
- package/dist/cli/tui/utils/json-tree.js +339 -0
- package/dist/cli/tui/utils/json-tree.js.map +1 -0
- package/dist/cli/tui/utils/open-external.d.ts +17 -0
- package/dist/cli/tui/utils/open-external.d.ts.map +1 -0
- package/dist/cli/tui/utils/open-external.js +57 -0
- package/dist/cli/tui/utils/open-external.js.map +1 -0
- package/dist/cli/tui/utils/syntax-highlight.d.ts +16 -0
- package/dist/cli/tui/utils/syntax-highlight.d.ts.map +1 -0
- package/dist/cli/tui/utils/syntax-highlight.js +64 -0
- package/dist/cli/tui/utils/syntax-highlight.js.map +1 -0
- package/dist/daemon/control.d.ts +3 -49
- package/dist/daemon/control.d.ts.map +1 -1
- package/dist/daemon/control.js +183 -141
- package/dist/daemon/control.js.map +1 -1
- package/dist/daemon/htpx-client.d.ts +8 -0
- package/dist/daemon/htpx-client.d.ts.map +1 -0
- package/dist/daemon/htpx-client.js +25 -0
- package/dist/daemon/htpx-client.js.map +1 -0
- package/dist/daemon/index.js +50 -2
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/interceptor-loader.d.ts +30 -0
- package/dist/daemon/interceptor-loader.d.ts.map +1 -0
- package/dist/daemon/interceptor-loader.js +249 -0
- package/dist/daemon/interceptor-loader.js.map +1 -0
- package/dist/daemon/interceptor-runner.d.ts +39 -0
- package/dist/daemon/interceptor-runner.d.ts.map +1 -0
- package/dist/daemon/interceptor-runner.js +312 -0
- package/dist/daemon/interceptor-runner.js.map +1 -0
- package/dist/daemon/proxy.d.ts +12 -0
- package/dist/daemon/proxy.d.ts.map +1 -1
- package/dist/daemon/proxy.js +121 -10
- package/dist/daemon/proxy.js.map +1 -1
- package/dist/daemon/storage.d.ts +64 -2
- package/dist/daemon/storage.d.ts.map +1 -1
- package/dist/daemon/storage.js +527 -12
- package/dist/daemon/storage.js.map +1 -1
- package/dist/interceptors.d.ts +2 -0
- package/dist/interceptors.d.ts.map +1 -0
- package/dist/interceptors.js +2 -0
- package/dist/interceptors.js.map +1 -0
- package/dist/mcp/server.d.ts +110 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +806 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/shared/config.d.ts +21 -0
- package/dist/shared/config.d.ts.map +1 -0
- package/dist/shared/config.js +83 -0
- package/dist/shared/config.js.map +1 -0
- package/dist/shared/content-type.d.ts +64 -0
- package/dist/shared/content-type.d.ts.map +1 -0
- package/dist/shared/content-type.js +145 -0
- package/dist/shared/content-type.js.map +1 -0
- package/dist/shared/control-client.d.ts +144 -0
- package/dist/shared/control-client.d.ts.map +1 -0
- package/dist/shared/control-client.js +272 -0
- package/dist/shared/control-client.js.map +1 -0
- package/dist/shared/daemon.d.ts.map +1 -1
- package/dist/shared/daemon.js +17 -4
- package/dist/shared/daemon.js.map +1 -1
- package/dist/shared/logger.d.ts +21 -5
- package/dist/shared/logger.d.ts.map +1 -1
- package/dist/shared/logger.js +100 -21
- package/dist/shared/logger.js.map +1 -1
- package/dist/shared/project.d.ts +16 -3
- package/dist/shared/project.d.ts.map +1 -1
- package/dist/shared/project.js +45 -5
- package/dist/shared/project.js.map +1 -1
- package/dist/shared/proxy-info.d.ts +10 -0
- package/dist/shared/proxy-info.d.ts.map +1 -0
- package/dist/shared/proxy-info.js +15 -0
- package/dist/shared/proxy-info.js.map +1 -0
- package/dist/shared/types.d.ts +95 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/package.json +24 -5
- package/skills/htpx/SKILL.md +228 -0
package/dist/daemon/storage.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import Database from "better-sqlite3";
|
|
2
2
|
import { v4 as uuidv4 } from "uuid";
|
|
3
3
|
import { createLogger } from "../shared/logger.js";
|
|
4
|
+
import { normaliseContentType, buildTextContentTypeSqlCondition, buildJsonContentTypeSqlCondition, } from "../shared/content-type.js";
|
|
5
|
+
import { DEFAULT_MAX_STORED_REQUESTS } from "../shared/config.js";
|
|
6
|
+
const DEFAULT_QUERY_LIMIT = 1000;
|
|
7
|
+
const EVICTION_CHECK_INTERVAL = 100;
|
|
4
8
|
const SCHEMA = `
|
|
5
9
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
6
10
|
id TEXT PRIMARY KEY,
|
|
@@ -20,10 +24,16 @@ CREATE TABLE IF NOT EXISTS requests (
|
|
|
20
24
|
path TEXT NOT NULL,
|
|
21
25
|
request_headers TEXT,
|
|
22
26
|
request_body BLOB,
|
|
27
|
+
request_body_truncated INTEGER DEFAULT 0,
|
|
23
28
|
response_status INTEGER,
|
|
24
29
|
response_headers TEXT,
|
|
25
30
|
response_body BLOB,
|
|
31
|
+
response_body_truncated INTEGER DEFAULT 0,
|
|
26
32
|
duration_ms INTEGER,
|
|
33
|
+
request_content_type TEXT,
|
|
34
|
+
response_content_type TEXT,
|
|
35
|
+
intercepted_by TEXT,
|
|
36
|
+
interception_type TEXT CHECK(interception_type IN ('modified', 'mocked')),
|
|
27
37
|
created_at INTEGER DEFAULT (unixepoch()),
|
|
28
38
|
FOREIGN KEY (session_id) REFERENCES sessions(id)
|
|
29
39
|
);
|
|
@@ -31,18 +41,226 @@ CREATE TABLE IF NOT EXISTS requests (
|
|
|
31
41
|
CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp DESC);
|
|
32
42
|
CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id);
|
|
33
43
|
CREATE INDEX IF NOT EXISTS idx_requests_label ON requests(label);
|
|
44
|
+
CREATE INDEX IF NOT EXISTS idx_requests_method ON requests(method);
|
|
45
|
+
CREATE INDEX IF NOT EXISTS idx_requests_status ON requests(response_status);
|
|
46
|
+
CREATE INDEX IF NOT EXISTS idx_requests_host ON requests(host);
|
|
34
47
|
`;
|
|
48
|
+
const MIGRATIONS = [
|
|
49
|
+
{
|
|
50
|
+
version: 1,
|
|
51
|
+
description: "Add body truncation tracking columns",
|
|
52
|
+
sql: `
|
|
53
|
+
ALTER TABLE requests ADD COLUMN request_body_truncated INTEGER DEFAULT 0;
|
|
54
|
+
ALTER TABLE requests ADD COLUMN response_body_truncated INTEGER DEFAULT 0;
|
|
55
|
+
`,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
version: 2,
|
|
59
|
+
description: "Add indices for method and status filtering",
|
|
60
|
+
sql: `
|
|
61
|
+
CREATE INDEX IF NOT EXISTS idx_requests_method ON requests(method);
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_requests_status ON requests(response_status);
|
|
63
|
+
`,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
version: 3,
|
|
67
|
+
description: "Add content-type columns for efficient body searching",
|
|
68
|
+
sql: `
|
|
69
|
+
ALTER TABLE requests ADD COLUMN request_content_type TEXT;
|
|
70
|
+
ALTER TABLE requests ADD COLUMN response_content_type TEXT;
|
|
71
|
+
`,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
version: 4,
|
|
75
|
+
description: "Add index on host for host-based filtering",
|
|
76
|
+
sql: `CREATE INDEX IF NOT EXISTS idx_requests_host ON requests(host);`,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
version: 5,
|
|
80
|
+
description: "Add interceptor tracking columns",
|
|
81
|
+
sql: `
|
|
82
|
+
ALTER TABLE requests ADD COLUMN intercepted_by TEXT;
|
|
83
|
+
ALTER TABLE requests ADD COLUMN interception_type TEXT;
|
|
84
|
+
`,
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
const STATUS_RANGE_MULTIPLIER = 100;
|
|
88
|
+
const MIN_HTTP_STATUS = 100;
|
|
89
|
+
const MAX_HTTP_STATUS = 599;
|
|
90
|
+
/**
|
|
91
|
+
* Escape SQL LIKE wildcards in user input to prevent unintended pattern matching.
|
|
92
|
+
*/
|
|
93
|
+
function escapeLikeWildcards(input) {
|
|
94
|
+
return input.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Apply status range filter condition. Supports three formats:
|
|
98
|
+
* - Nxx pattern (e.g. "2xx") → range from N00 to (N+1)00
|
|
99
|
+
* - Exact code (e.g. "401") → exact match
|
|
100
|
+
* - Numeric range (e.g. "500-503") → inclusive range
|
|
101
|
+
*/
|
|
102
|
+
function applyStatusCondition(conditions, params, statusRange) {
|
|
103
|
+
// Nxx pattern — e.g. "2xx", "4xx"
|
|
104
|
+
if (/^[1-5]xx$/.test(statusRange)) {
|
|
105
|
+
const firstDigit = parseInt(statusRange.charAt(0), 10);
|
|
106
|
+
const lower = firstDigit * STATUS_RANGE_MULTIPLIER;
|
|
107
|
+
const upper = (firstDigit + 1) * STATUS_RANGE_MULTIPLIER;
|
|
108
|
+
conditions.push("response_status >= ? AND response_status < ?");
|
|
109
|
+
params.push(lower, upper);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// Numeric range — e.g. "500-503"
|
|
113
|
+
const rangeMatch = statusRange.match(/^(\d{3})-(\d{3})$/);
|
|
114
|
+
if (rangeMatch && rangeMatch[1] && rangeMatch[2]) {
|
|
115
|
+
const low = parseInt(rangeMatch[1], 10);
|
|
116
|
+
const high = parseInt(rangeMatch[2], 10);
|
|
117
|
+
if (low >= MIN_HTTP_STATUS && high <= MAX_HTTP_STATUS && low <= high) {
|
|
118
|
+
conditions.push("response_status >= ? AND response_status <= ?");
|
|
119
|
+
params.push(low, high);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Exact code — e.g. "401"
|
|
124
|
+
if (/^\d{3}$/.test(statusRange)) {
|
|
125
|
+
const code = parseInt(statusRange, 10);
|
|
126
|
+
if (code >= MIN_HTTP_STATUS && code <= MAX_HTTP_STATUS) {
|
|
127
|
+
conditions.push("response_status = ?");
|
|
128
|
+
params.push(code);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Unrecognised format — silently ignored at the storage layer
|
|
133
|
+
// (validation should happen upstream in MCP/control server)
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Apply RequestFilter conditions to an existing SQL conditions/params array.
|
|
137
|
+
* Mutates both arrays in place.
|
|
138
|
+
*/
|
|
139
|
+
function applyFilterConditions(conditions, params, filter) {
|
|
140
|
+
if (!filter)
|
|
141
|
+
return;
|
|
142
|
+
if (filter.methods && filter.methods.length > 0) {
|
|
143
|
+
const placeholders = filter.methods.map(() => "?").join(", ");
|
|
144
|
+
conditions.push(`method IN (${placeholders})`);
|
|
145
|
+
params.push(...filter.methods);
|
|
146
|
+
}
|
|
147
|
+
if (filter.statusRange) {
|
|
148
|
+
applyStatusCondition(conditions, params, filter.statusRange);
|
|
149
|
+
}
|
|
150
|
+
if (filter.search) {
|
|
151
|
+
const escaped = escapeLikeWildcards(filter.search);
|
|
152
|
+
const pattern = `%${escaped}%`;
|
|
153
|
+
conditions.push("(url LIKE ? ESCAPE '\\' OR path LIKE ? ESCAPE '\\')");
|
|
154
|
+
params.push(pattern, pattern);
|
|
155
|
+
}
|
|
156
|
+
if (filter.host) {
|
|
157
|
+
if (filter.host.startsWith(".")) {
|
|
158
|
+
// Suffix match — e.g. ".example.com" matches "api.example.com"
|
|
159
|
+
const escaped = escapeLikeWildcards(filter.host);
|
|
160
|
+
conditions.push("host LIKE ? ESCAPE '\\'");
|
|
161
|
+
params.push(`%${escaped}`);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
// Exact match
|
|
165
|
+
conditions.push("host = ?");
|
|
166
|
+
params.push(filter.host);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (filter.pathPrefix) {
|
|
170
|
+
const escaped = escapeLikeWildcards(filter.pathPrefix);
|
|
171
|
+
conditions.push("path LIKE ? ESCAPE '\\'");
|
|
172
|
+
params.push(`${escaped}%`);
|
|
173
|
+
}
|
|
174
|
+
if (filter.since !== undefined) {
|
|
175
|
+
conditions.push("timestamp >= ?");
|
|
176
|
+
params.push(filter.since);
|
|
177
|
+
}
|
|
178
|
+
if (filter.before !== undefined) {
|
|
179
|
+
conditions.push("timestamp < ?");
|
|
180
|
+
params.push(filter.before);
|
|
181
|
+
}
|
|
182
|
+
if (filter.interceptedBy) {
|
|
183
|
+
conditions.push("intercepted_by = ?");
|
|
184
|
+
params.push(filter.interceptedBy);
|
|
185
|
+
}
|
|
186
|
+
if (filter.headerName) {
|
|
187
|
+
const name = filter.headerName.toLowerCase();
|
|
188
|
+
const jsonPath = `$."${name}"`;
|
|
189
|
+
const target = filter.headerTarget ?? "both";
|
|
190
|
+
if (filter.headerValue !== undefined) {
|
|
191
|
+
// Name + value match
|
|
192
|
+
if (target === "request") {
|
|
193
|
+
conditions.push("json_extract(request_headers, ?) = ?");
|
|
194
|
+
params.push(jsonPath, filter.headerValue);
|
|
195
|
+
}
|
|
196
|
+
else if (target === "response") {
|
|
197
|
+
conditions.push("json_extract(response_headers, ?) = ?");
|
|
198
|
+
params.push(jsonPath, filter.headerValue);
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
conditions.push("(json_extract(request_headers, ?) = ? OR json_extract(response_headers, ?) = ?)");
|
|
202
|
+
params.push(jsonPath, filter.headerValue, jsonPath, filter.headerValue);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
// Name-only existence check
|
|
207
|
+
if (target === "request") {
|
|
208
|
+
conditions.push("json_extract(request_headers, ?) IS NOT NULL");
|
|
209
|
+
params.push(jsonPath);
|
|
210
|
+
}
|
|
211
|
+
else if (target === "response") {
|
|
212
|
+
conditions.push("json_extract(response_headers, ?) IS NOT NULL");
|
|
213
|
+
params.push(jsonPath);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
conditions.push("(json_extract(request_headers, ?) IS NOT NULL OR json_extract(response_headers, ?) IS NOT NULL)");
|
|
217
|
+
params.push(jsonPath, jsonPath);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
35
222
|
export class RequestRepository {
|
|
36
223
|
db;
|
|
37
224
|
logger;
|
|
38
|
-
|
|
225
|
+
maxStoredRequests;
|
|
226
|
+
insertsSinceLastEvictionCheck = 0;
|
|
227
|
+
constructor(dbPath, projectRoot, logLevel, options) {
|
|
39
228
|
this.db = new Database(dbPath);
|
|
40
229
|
this.db.pragma("journal_mode = WAL");
|
|
41
230
|
this.db.exec(SCHEMA);
|
|
231
|
+
this.maxStoredRequests = options?.maxStoredRequests ?? DEFAULT_MAX_STORED_REQUESTS;
|
|
232
|
+
// Fresh databases already have the latest schema — stamp to latest version
|
|
233
|
+
// so migrations don't try to re-apply what's already in the CREATE TABLE.
|
|
234
|
+
const currentVersion = this.db.pragma("user_version", { simple: true });
|
|
235
|
+
if (currentVersion === 0) {
|
|
236
|
+
const hasData = this.db.prepare("SELECT COUNT(*) as count FROM requests").get().count > 0;
|
|
237
|
+
if (!hasData) {
|
|
238
|
+
const lastMigration = MIGRATIONS[MIGRATIONS.length - 1];
|
|
239
|
+
const latestVersion = lastMigration ? lastMigration.version : 0;
|
|
240
|
+
this.db.pragma(`user_version = ${latestVersion}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
this.applyMigrations();
|
|
42
244
|
if (projectRoot) {
|
|
43
245
|
this.logger = createLogger("storage", projectRoot, logLevel);
|
|
44
246
|
}
|
|
45
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Apply pending database migrations using SQLite's user_version pragma for tracking.
|
|
250
|
+
*/
|
|
251
|
+
applyMigrations() {
|
|
252
|
+
const currentVersion = this.db.pragma("user_version", { simple: true });
|
|
253
|
+
const pending = MIGRATIONS.filter((m) => m.version > currentVersion);
|
|
254
|
+
if (pending.length === 0)
|
|
255
|
+
return;
|
|
256
|
+
const applyAll = this.db.transaction(() => {
|
|
257
|
+
for (const migration of pending) {
|
|
258
|
+
this.db.exec(migration.sql);
|
|
259
|
+
this.db.pragma(`user_version = ${migration.version}`);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
applyAll();
|
|
263
|
+
}
|
|
46
264
|
/**
|
|
47
265
|
* Register a new session.
|
|
48
266
|
*/
|
|
@@ -122,33 +340,49 @@ export class RequestRepository {
|
|
|
122
340
|
*/
|
|
123
341
|
saveRequest(request) {
|
|
124
342
|
const id = uuidv4();
|
|
343
|
+
const requestContentType = request.requestHeaders
|
|
344
|
+
? normaliseContentType(request.requestHeaders["content-type"])
|
|
345
|
+
: null;
|
|
125
346
|
const stmt = this.db.prepare(`
|
|
126
347
|
INSERT INTO requests (
|
|
127
348
|
id, session_id, label, timestamp, method, url, host, path,
|
|
128
|
-
request_headers, request_body, response_status, response_headers,
|
|
129
|
-
response_body, duration_ms
|
|
349
|
+
request_headers, request_body, request_body_truncated, response_status, response_headers,
|
|
350
|
+
response_body, response_body_truncated, duration_ms, request_content_type
|
|
130
351
|
)
|
|
131
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
352
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
132
353
|
`);
|
|
133
|
-
stmt.run(id, request.sessionId, request.label ?? null, request.timestamp, request.method, request.url, request.host, request.path, request.requestHeaders ? JSON.stringify(request.requestHeaders) : null, request.requestBody ?? null, request.responseStatus ?? null, request.responseHeaders ? JSON.stringify(request.responseHeaders) : null, request.responseBody ?? null, request.durationMs ?? null);
|
|
354
|
+
stmt.run(id, request.sessionId, request.label ?? null, request.timestamp, request.method, request.url, request.host, request.path, request.requestHeaders ? JSON.stringify(request.requestHeaders) : null, request.requestBody ?? null, request.requestBodyTruncated ? 1 : 0, request.responseStatus ?? null, request.responseHeaders ? JSON.stringify(request.responseHeaders) : null, request.responseBody ?? null, request.responseBodyTruncated ? 1 : 0, request.durationMs ?? null, requestContentType);
|
|
134
355
|
this.logger?.debug("Request saved", {
|
|
135
356
|
id,
|
|
136
357
|
sessionId: request.sessionId,
|
|
137
358
|
method: request.method,
|
|
138
359
|
url: request.url,
|
|
139
360
|
});
|
|
361
|
+
this.evictIfNeeded();
|
|
140
362
|
return id;
|
|
141
363
|
}
|
|
142
364
|
/**
|
|
143
365
|
* Update a request with response data.
|
|
144
366
|
*/
|
|
145
367
|
updateRequestResponse(id, response) {
|
|
368
|
+
const responseContentType = normaliseContentType(response.headers["content-type"]);
|
|
146
369
|
const stmt = this.db.prepare(`
|
|
147
370
|
UPDATE requests
|
|
148
|
-
SET response_status = ?, response_headers = ?, response_body = ?, duration_ms = ?
|
|
371
|
+
SET response_status = ?, response_headers = ?, response_body = ?, response_body_truncated = ?, duration_ms = ?, response_content_type = ?
|
|
149
372
|
WHERE id = ?
|
|
150
373
|
`);
|
|
151
|
-
stmt.run(response.status, JSON.stringify(response.headers), response.body ?? null, response.durationMs, id);
|
|
374
|
+
stmt.run(response.status, JSON.stringify(response.headers), response.body ?? null, response.responseBodyTruncated ? 1 : 0, response.durationMs, responseContentType, id);
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Update a request with interceptor metadata.
|
|
378
|
+
*/
|
|
379
|
+
updateRequestInterception(id, interceptedBy, interceptionType) {
|
|
380
|
+
const stmt = this.db.prepare(`
|
|
381
|
+
UPDATE requests
|
|
382
|
+
SET intercepted_by = ?, interception_type = ?
|
|
383
|
+
WHERE id = ?
|
|
384
|
+
`);
|
|
385
|
+
stmt.run(interceptedBy, interceptionType, id);
|
|
152
386
|
}
|
|
153
387
|
/**
|
|
154
388
|
* Get a request by ID.
|
|
@@ -174,8 +408,9 @@ export class RequestRepository {
|
|
|
174
408
|
conditions.push("label = ?");
|
|
175
409
|
params.push(options.label);
|
|
176
410
|
}
|
|
411
|
+
applyFilterConditions(conditions, params, options.filter);
|
|
177
412
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
178
|
-
const limit = options.limit ??
|
|
413
|
+
const limit = options.limit ?? DEFAULT_QUERY_LIMIT;
|
|
179
414
|
const offset = options.offset ?? 0;
|
|
180
415
|
const stmt = this.db.prepare(`
|
|
181
416
|
SELECT * FROM requests
|
|
@@ -187,6 +422,50 @@ export class RequestRepository {
|
|
|
187
422
|
const rows = stmt.all(...params);
|
|
188
423
|
return rows.map((row) => this.rowToRequest(row));
|
|
189
424
|
}
|
|
425
|
+
/**
|
|
426
|
+
* List request summaries (excludes body/header data for performance).
|
|
427
|
+
* Use this for list views where full request data isn't needed.
|
|
428
|
+
*/
|
|
429
|
+
listRequestsSummary(options = {}) {
|
|
430
|
+
const conditions = [];
|
|
431
|
+
const params = [];
|
|
432
|
+
if (options.sessionId) {
|
|
433
|
+
conditions.push("session_id = ?");
|
|
434
|
+
params.push(options.sessionId);
|
|
435
|
+
}
|
|
436
|
+
if (options.label) {
|
|
437
|
+
conditions.push("label = ?");
|
|
438
|
+
params.push(options.label);
|
|
439
|
+
}
|
|
440
|
+
applyFilterConditions(conditions, params, options.filter);
|
|
441
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
442
|
+
const limit = options.limit ?? DEFAULT_QUERY_LIMIT;
|
|
443
|
+
const offset = options.offset ?? 0;
|
|
444
|
+
const stmt = this.db.prepare(`
|
|
445
|
+
SELECT
|
|
446
|
+
id,
|
|
447
|
+
session_id,
|
|
448
|
+
label,
|
|
449
|
+
timestamp,
|
|
450
|
+
method,
|
|
451
|
+
url,
|
|
452
|
+
host,
|
|
453
|
+
path,
|
|
454
|
+
response_status,
|
|
455
|
+
duration_ms,
|
|
456
|
+
COALESCE(LENGTH(request_body), 0) as request_body_size,
|
|
457
|
+
COALESCE(LENGTH(response_body), 0) as response_body_size,
|
|
458
|
+
intercepted_by,
|
|
459
|
+
interception_type
|
|
460
|
+
FROM requests
|
|
461
|
+
${whereClause}
|
|
462
|
+
ORDER BY timestamp DESC
|
|
463
|
+
LIMIT ? OFFSET ?
|
|
464
|
+
`);
|
|
465
|
+
params.push(limit, offset);
|
|
466
|
+
const rows = stmt.all(...params);
|
|
467
|
+
return rows.map((row) => this.rowToSummary(row));
|
|
468
|
+
}
|
|
190
469
|
/**
|
|
191
470
|
* Count requests, optionally filtered by session or label.
|
|
192
471
|
*/
|
|
@@ -201,6 +480,7 @@ export class RequestRepository {
|
|
|
201
480
|
conditions.push("label = ?");
|
|
202
481
|
params.push(options.label);
|
|
203
482
|
}
|
|
483
|
+
applyFilterConditions(conditions, params, options.filter);
|
|
204
484
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
205
485
|
const stmt = this.db.prepare(`
|
|
206
486
|
SELECT COUNT(*) as count FROM requests ${whereClause}
|
|
@@ -208,18 +488,249 @@ export class RequestRepository {
|
|
|
208
488
|
const result = stmt.get(...params);
|
|
209
489
|
return result.count;
|
|
210
490
|
}
|
|
491
|
+
/**
|
|
492
|
+
* Search through request/response body content for a text pattern.
|
|
493
|
+
* Only searches text-based bodies (not binary).
|
|
494
|
+
*/
|
|
495
|
+
searchBodies(options) {
|
|
496
|
+
const conditions = [];
|
|
497
|
+
const params = [];
|
|
498
|
+
const escaped = escapeLikeWildcards(options.query);
|
|
499
|
+
const pattern = `%${escaped}%`;
|
|
500
|
+
// Build content-type conditions — only search text-based bodies
|
|
501
|
+
const reqCt = buildTextContentTypeSqlCondition("request_content_type");
|
|
502
|
+
const resCt = buildTextContentTypeSqlCondition("response_content_type");
|
|
503
|
+
// Search in both request and response bodies, but only where the content type is text-based
|
|
504
|
+
conditions.push(`((${reqCt.clause} AND CAST(request_body AS TEXT) LIKE ? ESCAPE '\\') OR (${resCt.clause} AND CAST(response_body AS TEXT) LIKE ? ESCAPE '\\'))`);
|
|
505
|
+
params.push(...reqCt.params, pattern, ...resCt.params, pattern);
|
|
506
|
+
applyFilterConditions(conditions, params, options.filter);
|
|
507
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
508
|
+
const limit = options.limit ?? DEFAULT_QUERY_LIMIT;
|
|
509
|
+
const offset = options.offset ?? 0;
|
|
510
|
+
const stmt = this.db.prepare(`
|
|
511
|
+
SELECT
|
|
512
|
+
id,
|
|
513
|
+
session_id,
|
|
514
|
+
label,
|
|
515
|
+
timestamp,
|
|
516
|
+
method,
|
|
517
|
+
url,
|
|
518
|
+
host,
|
|
519
|
+
path,
|
|
520
|
+
response_status,
|
|
521
|
+
duration_ms,
|
|
522
|
+
COALESCE(LENGTH(request_body), 0) as request_body_size,
|
|
523
|
+
COALESCE(LENGTH(response_body), 0) as response_body_size,
|
|
524
|
+
intercepted_by,
|
|
525
|
+
interception_type
|
|
526
|
+
FROM requests
|
|
527
|
+
${whereClause}
|
|
528
|
+
ORDER BY timestamp DESC
|
|
529
|
+
LIMIT ? OFFSET ?
|
|
530
|
+
`);
|
|
531
|
+
params.push(limit, offset);
|
|
532
|
+
const rows = stmt.all(...params);
|
|
533
|
+
return rows.map((row) => this.rowToSummary(row));
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Query JSON bodies using SQLite's json_extract.
|
|
537
|
+
* Only queries rows with JSON content types.
|
|
538
|
+
*/
|
|
539
|
+
queryJsonBodies(options) {
|
|
540
|
+
const target = options.target ?? "both";
|
|
541
|
+
const conditions = [];
|
|
542
|
+
const params = [];
|
|
543
|
+
// Build the JSON extraction expressions per target
|
|
544
|
+
const extractParts = [];
|
|
545
|
+
if (target === "request" || target === "both") {
|
|
546
|
+
const reqCt = buildJsonContentTypeSqlCondition("request_content_type");
|
|
547
|
+
const reqExtract = `CASE WHEN ${reqCt.clause} THEN json_extract(CAST(request_body AS TEXT), ?) ELSE NULL END`;
|
|
548
|
+
extractParts.push({ sql: reqExtract, ctParams: reqCt.params, column: "request" });
|
|
549
|
+
}
|
|
550
|
+
if (target === "response" || target === "both") {
|
|
551
|
+
const resCt = buildJsonContentTypeSqlCondition("response_content_type");
|
|
552
|
+
const resExtract = `CASE WHEN ${resCt.clause} THEN json_extract(CAST(response_body AS TEXT), ?) ELSE NULL END`;
|
|
553
|
+
extractParts.push({ sql: resExtract, ctParams: resCt.params, column: "response" });
|
|
554
|
+
}
|
|
555
|
+
// Build the select with extracted value, preferring request over response for "both"
|
|
556
|
+
const extracts = extractParts;
|
|
557
|
+
const extractSelectParts = [];
|
|
558
|
+
const extractSelectParams = [];
|
|
559
|
+
for (const part of extracts) {
|
|
560
|
+
extractSelectParts.push(part.sql);
|
|
561
|
+
extractSelectParams.push(...part.ctParams, options.jsonPath);
|
|
562
|
+
}
|
|
563
|
+
// COALESCE so "both" returns the first non-null value
|
|
564
|
+
const extractedValueExpr = extractSelectParts.length > 1
|
|
565
|
+
? `COALESCE(${extractSelectParts.join(", ")})`
|
|
566
|
+
: (extractSelectParts[0] ?? "NULL");
|
|
567
|
+
// Content-type restriction: at least one target must have a JSON content type
|
|
568
|
+
const ctConditions = [];
|
|
569
|
+
const ctParams = [];
|
|
570
|
+
if (target === "request" || target === "both") {
|
|
571
|
+
const reqCt = buildJsonContentTypeSqlCondition("request_content_type");
|
|
572
|
+
ctConditions.push(reqCt.clause);
|
|
573
|
+
ctParams.push(...reqCt.params);
|
|
574
|
+
}
|
|
575
|
+
if (target === "response" || target === "both") {
|
|
576
|
+
const resCt = buildJsonContentTypeSqlCondition("response_content_type");
|
|
577
|
+
ctConditions.push(resCt.clause);
|
|
578
|
+
ctParams.push(...resCt.params);
|
|
579
|
+
}
|
|
580
|
+
conditions.push(`(${ctConditions.join(" OR ")})`);
|
|
581
|
+
params.push(...ctParams);
|
|
582
|
+
applyFilterConditions(conditions, params, options.filter);
|
|
583
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
584
|
+
const limit = options.limit ?? DEFAULT_QUERY_LIMIT;
|
|
585
|
+
const offset = options.offset ?? 0;
|
|
586
|
+
// Build the full query — we use a subquery to compute extracted_value,
|
|
587
|
+
// then filter on it in the outer query
|
|
588
|
+
let sql;
|
|
589
|
+
const allParams = [];
|
|
590
|
+
if (options.value !== undefined) {
|
|
591
|
+
sql = `
|
|
592
|
+
SELECT * FROM (
|
|
593
|
+
SELECT
|
|
594
|
+
id,
|
|
595
|
+
session_id,
|
|
596
|
+
label,
|
|
597
|
+
timestamp,
|
|
598
|
+
method,
|
|
599
|
+
url,
|
|
600
|
+
host,
|
|
601
|
+
path,
|
|
602
|
+
response_status,
|
|
603
|
+
duration_ms,
|
|
604
|
+
COALESCE(LENGTH(request_body), 0) as request_body_size,
|
|
605
|
+
COALESCE(LENGTH(response_body), 0) as response_body_size,
|
|
606
|
+
${extractedValueExpr} as extracted_value
|
|
607
|
+
FROM requests
|
|
608
|
+
${whereClause}
|
|
609
|
+
) sub
|
|
610
|
+
WHERE extracted_value = ?
|
|
611
|
+
ORDER BY timestamp DESC
|
|
612
|
+
LIMIT ? OFFSET ?
|
|
613
|
+
`;
|
|
614
|
+
allParams.push(...extractSelectParams, ...params, options.value, limit, offset);
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
sql = `
|
|
618
|
+
SELECT * FROM (
|
|
619
|
+
SELECT
|
|
620
|
+
id,
|
|
621
|
+
session_id,
|
|
622
|
+
label,
|
|
623
|
+
timestamp,
|
|
624
|
+
method,
|
|
625
|
+
url,
|
|
626
|
+
host,
|
|
627
|
+
path,
|
|
628
|
+
response_status,
|
|
629
|
+
duration_ms,
|
|
630
|
+
COALESCE(LENGTH(request_body), 0) as request_body_size,
|
|
631
|
+
COALESCE(LENGTH(response_body), 0) as response_body_size,
|
|
632
|
+
${extractedValueExpr} as extracted_value
|
|
633
|
+
FROM requests
|
|
634
|
+
${whereClause}
|
|
635
|
+
) sub
|
|
636
|
+
WHERE extracted_value IS NOT NULL
|
|
637
|
+
ORDER BY timestamp DESC
|
|
638
|
+
LIMIT ? OFFSET ?
|
|
639
|
+
`;
|
|
640
|
+
allParams.push(...extractSelectParams, ...params, limit, offset);
|
|
641
|
+
}
|
|
642
|
+
const stmt = this.db.prepare(sql);
|
|
643
|
+
const rows = stmt.all(...allParams);
|
|
644
|
+
return rows.map((row) => ({
|
|
645
|
+
id: row.id,
|
|
646
|
+
sessionId: row.session_id,
|
|
647
|
+
label: row.label ?? undefined,
|
|
648
|
+
timestamp: row.timestamp,
|
|
649
|
+
method: row.method,
|
|
650
|
+
url: row.url,
|
|
651
|
+
host: row.host,
|
|
652
|
+
path: row.path,
|
|
653
|
+
responseStatus: row.response_status ?? undefined,
|
|
654
|
+
durationMs: row.duration_ms ?? undefined,
|
|
655
|
+
requestBodySize: row.request_body_size,
|
|
656
|
+
responseBodySize: row.response_body_size,
|
|
657
|
+
extractedValue: row.extracted_value,
|
|
658
|
+
}));
|
|
659
|
+
}
|
|
211
660
|
/**
|
|
212
661
|
* Delete all requests (useful for cleanup).
|
|
213
662
|
*/
|
|
214
663
|
clearRequests() {
|
|
215
664
|
this.db.exec("DELETE FROM requests");
|
|
216
665
|
}
|
|
666
|
+
/**
|
|
667
|
+
* Reclaim disk space by checkpointing the WAL and vacuuming.
|
|
668
|
+
* Intended for use during shutdown — not suitable for the hot path.
|
|
669
|
+
*/
|
|
670
|
+
compactDatabase() {
|
|
671
|
+
this.db.pragma("wal_checkpoint(TRUNCATE)");
|
|
672
|
+
this.db.exec("VACUUM");
|
|
673
|
+
}
|
|
217
674
|
/**
|
|
218
675
|
* Close the database connection.
|
|
219
676
|
*/
|
|
220
677
|
close() {
|
|
221
678
|
this.db.close();
|
|
222
679
|
}
|
|
680
|
+
/**
|
|
681
|
+
* Check whether the request count exceeds the cap and evict oldest rows.
|
|
682
|
+
* Only runs the actual COUNT query every EVICTION_CHECK_INTERVAL inserts
|
|
683
|
+
* to keep the hot path cheap.
|
|
684
|
+
*/
|
|
685
|
+
evictIfNeeded() {
|
|
686
|
+
this.insertsSinceLastEvictionCheck++;
|
|
687
|
+
if (this.insertsSinceLastEvictionCheck < EVICTION_CHECK_INTERVAL) {
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
this.insertsSinceLastEvictionCheck = 0;
|
|
691
|
+
const { count } = this.db.prepare("SELECT COUNT(*) as count FROM requests").get();
|
|
692
|
+
if (count <= this.maxStoredRequests) {
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
const excess = count - this.maxStoredRequests;
|
|
696
|
+
this.db
|
|
697
|
+
.prepare(`DELETE FROM requests WHERE id IN (
|
|
698
|
+
SELECT id FROM requests ORDER BY timestamp ASC LIMIT ?
|
|
699
|
+
)`)
|
|
700
|
+
.run(excess);
|
|
701
|
+
this.logger?.debug("Evicted old requests", {
|
|
702
|
+
evicted: excess,
|
|
703
|
+
remaining: this.maxStoredRequests,
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
rowToSummary(row) {
|
|
707
|
+
return {
|
|
708
|
+
id: row.id,
|
|
709
|
+
sessionId: row.session_id,
|
|
710
|
+
label: row.label ?? undefined,
|
|
711
|
+
timestamp: row.timestamp,
|
|
712
|
+
method: row.method,
|
|
713
|
+
url: row.url,
|
|
714
|
+
host: row.host,
|
|
715
|
+
path: row.path,
|
|
716
|
+
responseStatus: row.response_status ?? undefined,
|
|
717
|
+
durationMs: row.duration_ms ?? undefined,
|
|
718
|
+
requestBodySize: row.request_body_size,
|
|
719
|
+
responseBodySize: row.response_body_size,
|
|
720
|
+
interceptedBy: row.intercepted_by ?? undefined,
|
|
721
|
+
interceptionType: row.interception_type === "modified" || row.interception_type === "mocked"
|
|
722
|
+
? row.interception_type
|
|
723
|
+
: undefined,
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
safeParseHeaders(json) {
|
|
727
|
+
try {
|
|
728
|
+
return JSON.parse(json);
|
|
729
|
+
}
|
|
730
|
+
catch {
|
|
731
|
+
return {};
|
|
732
|
+
}
|
|
733
|
+
}
|
|
223
734
|
rowToRequest(row) {
|
|
224
735
|
return {
|
|
225
736
|
id: row.id,
|
|
@@ -230,16 +741,20 @@ export class RequestRepository {
|
|
|
230
741
|
url: row.url,
|
|
231
742
|
host: row.host,
|
|
232
743
|
path: row.path,
|
|
233
|
-
requestHeaders: row.request_headers
|
|
234
|
-
? JSON.parse(row.request_headers)
|
|
235
|
-
: {},
|
|
744
|
+
requestHeaders: row.request_headers ? this.safeParseHeaders(row.request_headers) : {},
|
|
236
745
|
requestBody: row.request_body ?? undefined,
|
|
746
|
+
requestBodyTruncated: row.request_body_truncated === 1,
|
|
237
747
|
responseStatus: row.response_status ?? undefined,
|
|
238
748
|
responseHeaders: row.response_headers
|
|
239
|
-
?
|
|
749
|
+
? this.safeParseHeaders(row.response_headers)
|
|
240
750
|
: undefined,
|
|
241
751
|
responseBody: row.response_body ?? undefined,
|
|
752
|
+
responseBodyTruncated: row.response_body_truncated === 1,
|
|
242
753
|
durationMs: row.duration_ms ?? undefined,
|
|
754
|
+
interceptedBy: row.intercepted_by ?? undefined,
|
|
755
|
+
interceptionType: row.interception_type === "modified" || row.interception_type === "mocked"
|
|
756
|
+
? row.interception_type
|
|
757
|
+
: undefined,
|
|
243
758
|
};
|
|
244
759
|
}
|
|
245
760
|
}
|