procsi 0.2.6
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 -0
- package/README.md +587 -0
- package/dist/cli/commands/clear.d.ts +3 -0
- package/dist/cli/commands/clear.d.ts.map +1 -0
- package/dist/cli/commands/clear.js +30 -0
- package/dist/cli/commands/clear.js.map +1 -0
- 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 +27 -0
- package/dist/cli/commands/debug-dump.d.ts.map +1 -0
- package/dist/cli/commands/debug-dump.js +102 -0
- package/dist/cli/commands/debug-dump.js.map +1 -0
- 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 +9 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +28 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/intercept.d.ts +9 -0
- package/dist/cli/commands/intercept.d.ts.map +1 -0
- package/dist/cli/commands/intercept.js +121 -0
- package/dist/cli/commands/intercept.js.map +1 -0
- 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 +3 -0
- package/dist/cli/commands/project.d.ts.map +1 -0
- package/dist/cli/commands/project.js +15 -0
- package/dist/cli/commands/project.js.map +1 -0
- package/dist/cli/commands/restart.d.ts +3 -0
- package/dist/cli/commands/restart.d.ts.map +1 -0
- package/dist/cli/commands/restart.js +35 -0
- package/dist/cli/commands/restart.js.map +1 -0
- package/dist/cli/commands/status.d.ts +3 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +66 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/stop.d.ts +3 -0
- package/dist/cli/commands/stop.d.ts.map +1 -0
- package/dist/cli/commands/stop.js +24 -0
- package/dist/cli/commands/stop.js.map +1 -0
- package/dist/cli/commands/tui.d.ts +3 -0
- package/dist/cli/commands/tui.d.ts.map +1 -0
- package/dist/cli/commands/tui.js +36 -0
- package/dist/cli/commands/tui.js.map +1 -0
- package/dist/cli/commands/vars.d.ts +36 -0
- package/dist/cli/commands/vars.d.ts.map +1 -0
- package/dist/cli/commands/vars.js +207 -0
- package/dist/cli/commands/vars.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +37 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/tui/App.d.ts +15 -0
- package/dist/cli/tui/App.d.ts.map +1 -0
- package/dist/cli/tui/App.js +544 -0
- package/dist/cli/tui/App.js.map +1 -0
- 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/BodyView.d.ts +14 -0
- package/dist/cli/tui/components/BodyView.d.ts.map +1 -0
- package/dist/cli/tui/components/BodyView.js +39 -0
- package/dist/cli/tui/components/BodyView.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/HeadersView.d.ts +13 -0
- package/dist/cli/tui/components/HeadersView.d.ts.map +1 -0
- package/dist/cli/tui/components/HeadersView.js +8 -0
- package/dist/cli/tui/components/HeadersView.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 +16 -0
- package/dist/cli/tui/components/RequestDetails.d.ts.map +1 -0
- package/dist/cli/tui/components/RequestDetails.js +23 -0
- package/dist/cli/tui/components/RequestDetails.js.map +1 -0
- package/dist/cli/tui/components/RequestList.d.ts +21 -0
- package/dist/cli/tui/components/RequestList.d.ts.map +1 -0
- package/dist/cli/tui/components/RequestList.js +30 -0
- package/dist/cli/tui/components/RequestList.js.map +1 -0
- package/dist/cli/tui/components/RequestListItem.d.ts +36 -0
- package/dist/cli/tui/components/RequestListItem.d.ts.map +1 -0
- package/dist/cli/tui/components/RequestListItem.js +130 -0
- package/dist/cli/tui/components/RequestListItem.js.map +1 -0
- 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 +39 -0
- package/dist/cli/tui/components/StatusBar.d.ts.map +1 -0
- package/dist/cli/tui/components/StatusBar.js +53 -0
- package/dist/cli/tui/components/StatusBar.js.map +1 -0
- 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 +29 -0
- package/dist/cli/tui/hooks/useExport.d.ts.map +1 -0
- package/dist/cli/tui/hooks/useExport.js +64 -0
- package/dist/cli/tui/hooks/useExport.js.map +1 -0
- package/dist/cli/tui/hooks/useRequests.d.ts +26 -0
- package/dist/cli/tui/hooks/useRequests.d.ts.map +1 -0
- package/dist/cli/tui/hooks/useRequests.js +131 -0
- package/dist/cli/tui/hooks/useRequests.js.map +1 -0
- 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/hooks/useStdoutDimensions.d.ts +11 -0
- package/dist/cli/tui/hooks/useStdoutDimensions.d.ts.map +1 -0
- package/dist/cli/tui/hooks/useStdoutDimensions.js +29 -0
- package/dist/cli/tui/hooks/useStdoutDimensions.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 +9 -0
- package/dist/cli/tui/utils/curl.d.ts.map +1 -0
- package/dist/cli/tui/utils/curl.js +54 -0
- package/dist/cli/tui/utils/curl.js.map +1 -0
- 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 +49 -0
- package/dist/cli/tui/utils/formatters.d.ts.map +1 -0
- package/dist/cli/tui/utils/formatters.js +200 -0
- package/dist/cli/tui/utils/formatters.js.map +1 -0
- package/dist/cli/tui/utils/har.d.ts +75 -0
- package/dist/cli/tui/utils/har.d.ts.map +1 -0
- package/dist/cli/tui/utils/har.js +117 -0
- package/dist/cli/tui/utils/har.js.map +1 -0
- 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 +21 -0
- package/dist/daemon/control.d.ts.map +1 -0
- package/dist/daemon/control.js +311 -0
- package/dist/daemon/control.js.map +1 -0
- 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.d.ts +3 -0
- package/dist/daemon/index.d.ts.map +1 -0
- package/dist/daemon/index.js +178 -0
- package/dist/daemon/index.js.map +1 -0
- 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/procsi-client.d.ts +8 -0
- package/dist/daemon/procsi-client.d.ts.map +1 -0
- package/dist/daemon/procsi-client.js +25 -0
- package/dist/daemon/procsi-client.js.map +1 -0
- package/dist/daemon/proxy.d.ts +34 -0
- package/dist/daemon/proxy.d.ts.map +1 -0
- package/dist/daemon/proxy.js +213 -0
- package/dist/daemon/proxy.js.map +1 -0
- package/dist/daemon/storage.d.ts +130 -0
- package/dist/daemon/storage.d.ts.map +1 -0
- package/dist/daemon/storage.js +761 -0
- package/dist/daemon/storage.js.map +1 -0
- 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/overrides/node.d.ts +30 -0
- package/dist/overrides/node.d.ts.map +1 -0
- package/dist/overrides/node.js +66 -0
- package/dist/overrides/node.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 +33 -0
- package/dist/shared/daemon.d.ts.map +1 -0
- package/dist/shared/daemon.js +231 -0
- package/dist/shared/daemon.js.map +1 -0
- package/dist/shared/logger.d.ts +47 -0
- package/dist/shared/logger.d.ts.map +1 -0
- package/dist/shared/logger.js +200 -0
- package/dist/shared/logger.js.map +1 -0
- package/dist/shared/project.d.ts +76 -0
- package/dist/shared/project.d.ts.map +1 -0
- package/dist/shared/project.js +185 -0
- package/dist/shared/project.js.map +1 -0
- 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 +128 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +5 -0
- package/dist/shared/types.js.map +1 -0
- package/dist/shared/version.d.ts +5 -0
- package/dist/shared/version.d.ts.map +1 -0
- package/dist/shared/version.js +21 -0
- package/dist/shared/version.js.map +1 -0
- package/package.json +113 -0
- package/skills/procsi/SKILL.md +228 -0
|
@@ -0,0 +1,761 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { v4 as uuidv4 } from "uuid";
|
|
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;
|
|
8
|
+
const SCHEMA = `
|
|
9
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
10
|
+
id TEXT PRIMARY KEY,
|
|
11
|
+
label TEXT,
|
|
12
|
+
pid INTEGER NOT NULL,
|
|
13
|
+
started_at INTEGER NOT NULL
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
CREATE TABLE IF NOT EXISTS requests (
|
|
17
|
+
id TEXT PRIMARY KEY,
|
|
18
|
+
session_id TEXT NOT NULL,
|
|
19
|
+
label TEXT,
|
|
20
|
+
timestamp INTEGER NOT NULL,
|
|
21
|
+
method TEXT NOT NULL,
|
|
22
|
+
url TEXT NOT NULL,
|
|
23
|
+
host TEXT NOT NULL,
|
|
24
|
+
path TEXT NOT NULL,
|
|
25
|
+
request_headers TEXT,
|
|
26
|
+
request_body BLOB,
|
|
27
|
+
request_body_truncated INTEGER DEFAULT 0,
|
|
28
|
+
response_status INTEGER,
|
|
29
|
+
response_headers TEXT,
|
|
30
|
+
response_body BLOB,
|
|
31
|
+
response_body_truncated INTEGER DEFAULT 0,
|
|
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')),
|
|
37
|
+
created_at INTEGER DEFAULT (unixepoch()),
|
|
38
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id)
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp DESC);
|
|
42
|
+
CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id);
|
|
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);
|
|
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
|
+
}
|
|
222
|
+
export class RequestRepository {
|
|
223
|
+
db;
|
|
224
|
+
logger;
|
|
225
|
+
maxStoredRequests;
|
|
226
|
+
insertsSinceLastEvictionCheck = 0;
|
|
227
|
+
constructor(dbPath, projectRoot, logLevel, options) {
|
|
228
|
+
this.db = new Database(dbPath);
|
|
229
|
+
this.db.pragma("journal_mode = WAL");
|
|
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();
|
|
244
|
+
if (projectRoot) {
|
|
245
|
+
this.logger = createLogger("storage", projectRoot, logLevel);
|
|
246
|
+
}
|
|
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
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Register a new session.
|
|
266
|
+
*/
|
|
267
|
+
registerSession(label, pid = process.pid) {
|
|
268
|
+
const session = {
|
|
269
|
+
id: uuidv4(),
|
|
270
|
+
label,
|
|
271
|
+
pid,
|
|
272
|
+
startedAt: Date.now(),
|
|
273
|
+
};
|
|
274
|
+
const stmt = this.db.prepare(`
|
|
275
|
+
INSERT INTO sessions (id, label, pid, started_at)
|
|
276
|
+
VALUES (?, ?, ?, ?)
|
|
277
|
+
`);
|
|
278
|
+
stmt.run(session.id, session.label ?? null, session.pid, session.startedAt);
|
|
279
|
+
return session;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Ensure a session exists with a specific ID.
|
|
283
|
+
* If the session already exists, returns it unchanged.
|
|
284
|
+
* If not, creates a new session with the given ID.
|
|
285
|
+
*/
|
|
286
|
+
ensureSession(id, label, pid = process.pid) {
|
|
287
|
+
const startedAt = Date.now();
|
|
288
|
+
const stmt = this.db.prepare(`
|
|
289
|
+
INSERT OR IGNORE INTO sessions (id, label, pid, started_at)
|
|
290
|
+
VALUES (?, ?, ?, ?)
|
|
291
|
+
`);
|
|
292
|
+
stmt.run(id, label ?? null, pid, startedAt);
|
|
293
|
+
// Return the session (either newly created or existing)
|
|
294
|
+
const existing = this.getSession(id);
|
|
295
|
+
if (existing) {
|
|
296
|
+
return existing;
|
|
297
|
+
}
|
|
298
|
+
// This should never happen since we just inserted, but satisfies the type checker
|
|
299
|
+
return { id, label, pid, startedAt };
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Get a session by ID.
|
|
303
|
+
*/
|
|
304
|
+
getSession(id) {
|
|
305
|
+
const stmt = this.db.prepare(`
|
|
306
|
+
SELECT id, label, pid, started_at as startedAt
|
|
307
|
+
FROM sessions
|
|
308
|
+
WHERE id = ?
|
|
309
|
+
`);
|
|
310
|
+
const row = stmt.get(id);
|
|
311
|
+
if (!row) {
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
id: row.id,
|
|
316
|
+
label: row.label ?? undefined,
|
|
317
|
+
pid: row.pid,
|
|
318
|
+
startedAt: row.startedAt,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* List all sessions.
|
|
323
|
+
*/
|
|
324
|
+
listSessions() {
|
|
325
|
+
const stmt = this.db.prepare(`
|
|
326
|
+
SELECT id, label, pid, started_at as startedAt
|
|
327
|
+
FROM sessions
|
|
328
|
+
ORDER BY started_at DESC
|
|
329
|
+
`);
|
|
330
|
+
const rows = stmt.all();
|
|
331
|
+
return rows.map((row) => ({
|
|
332
|
+
id: row.id,
|
|
333
|
+
label: row.label ?? undefined,
|
|
334
|
+
pid: row.pid,
|
|
335
|
+
startedAt: row.startedAt,
|
|
336
|
+
}));
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Save a captured request. Returns the generated ID.
|
|
340
|
+
*/
|
|
341
|
+
saveRequest(request) {
|
|
342
|
+
const id = uuidv4();
|
|
343
|
+
const requestContentType = request.requestHeaders
|
|
344
|
+
? normaliseContentType(request.requestHeaders["content-type"])
|
|
345
|
+
: null;
|
|
346
|
+
const stmt = this.db.prepare(`
|
|
347
|
+
INSERT INTO requests (
|
|
348
|
+
id, session_id, label, timestamp, method, url, host, path,
|
|
349
|
+
request_headers, request_body, request_body_truncated, response_status, response_headers,
|
|
350
|
+
response_body, response_body_truncated, duration_ms, request_content_type
|
|
351
|
+
)
|
|
352
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
353
|
+
`);
|
|
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);
|
|
355
|
+
this.logger?.debug("Request saved", {
|
|
356
|
+
id,
|
|
357
|
+
sessionId: request.sessionId,
|
|
358
|
+
method: request.method,
|
|
359
|
+
url: request.url,
|
|
360
|
+
});
|
|
361
|
+
this.evictIfNeeded();
|
|
362
|
+
return id;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Update a request with response data.
|
|
366
|
+
*/
|
|
367
|
+
updateRequestResponse(id, response) {
|
|
368
|
+
const responseContentType = normaliseContentType(response.headers["content-type"]);
|
|
369
|
+
const stmt = this.db.prepare(`
|
|
370
|
+
UPDATE requests
|
|
371
|
+
SET response_status = ?, response_headers = ?, response_body = ?, response_body_truncated = ?, duration_ms = ?, response_content_type = ?
|
|
372
|
+
WHERE id = ?
|
|
373
|
+
`);
|
|
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);
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Get a request by ID.
|
|
389
|
+
*/
|
|
390
|
+
getRequest(id) {
|
|
391
|
+
const stmt = this.db.prepare(`
|
|
392
|
+
SELECT * FROM requests WHERE id = ?
|
|
393
|
+
`);
|
|
394
|
+
const row = stmt.get(id);
|
|
395
|
+
return row ? this.rowToRequest(row) : undefined;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* List requests, optionally filtered by session or label.
|
|
399
|
+
*/
|
|
400
|
+
listRequests(options = {}) {
|
|
401
|
+
const conditions = [];
|
|
402
|
+
const params = [];
|
|
403
|
+
if (options.sessionId) {
|
|
404
|
+
conditions.push("session_id = ?");
|
|
405
|
+
params.push(options.sessionId);
|
|
406
|
+
}
|
|
407
|
+
if (options.label) {
|
|
408
|
+
conditions.push("label = ?");
|
|
409
|
+
params.push(options.label);
|
|
410
|
+
}
|
|
411
|
+
applyFilterConditions(conditions, params, options.filter);
|
|
412
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
413
|
+
const limit = options.limit ?? DEFAULT_QUERY_LIMIT;
|
|
414
|
+
const offset = options.offset ?? 0;
|
|
415
|
+
const stmt = this.db.prepare(`
|
|
416
|
+
SELECT * FROM requests
|
|
417
|
+
${whereClause}
|
|
418
|
+
ORDER BY timestamp DESC
|
|
419
|
+
LIMIT ? OFFSET ?
|
|
420
|
+
`);
|
|
421
|
+
params.push(limit, offset);
|
|
422
|
+
const rows = stmt.all(...params);
|
|
423
|
+
return rows.map((row) => this.rowToRequest(row));
|
|
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
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Count requests, optionally filtered by session or label.
|
|
471
|
+
*/
|
|
472
|
+
countRequests(options = {}) {
|
|
473
|
+
const conditions = [];
|
|
474
|
+
const params = [];
|
|
475
|
+
if (options.sessionId) {
|
|
476
|
+
conditions.push("session_id = ?");
|
|
477
|
+
params.push(options.sessionId);
|
|
478
|
+
}
|
|
479
|
+
if (options.label) {
|
|
480
|
+
conditions.push("label = ?");
|
|
481
|
+
params.push(options.label);
|
|
482
|
+
}
|
|
483
|
+
applyFilterConditions(conditions, params, options.filter);
|
|
484
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
485
|
+
const stmt = this.db.prepare(`
|
|
486
|
+
SELECT COUNT(*) as count FROM requests ${whereClause}
|
|
487
|
+
`);
|
|
488
|
+
const result = stmt.get(...params);
|
|
489
|
+
return result.count;
|
|
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
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Delete all requests (useful for cleanup).
|
|
662
|
+
*/
|
|
663
|
+
clearRequests() {
|
|
664
|
+
this.db.exec("DELETE FROM requests");
|
|
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
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Close the database connection.
|
|
676
|
+
*/
|
|
677
|
+
close() {
|
|
678
|
+
this.db.close();
|
|
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
|
+
}
|
|
734
|
+
rowToRequest(row) {
|
|
735
|
+
return {
|
|
736
|
+
id: row.id,
|
|
737
|
+
sessionId: row.session_id,
|
|
738
|
+
label: row.label ?? undefined,
|
|
739
|
+
timestamp: row.timestamp,
|
|
740
|
+
method: row.method,
|
|
741
|
+
url: row.url,
|
|
742
|
+
host: row.host,
|
|
743
|
+
path: row.path,
|
|
744
|
+
requestHeaders: row.request_headers ? this.safeParseHeaders(row.request_headers) : {},
|
|
745
|
+
requestBody: row.request_body ?? undefined,
|
|
746
|
+
requestBodyTruncated: row.request_body_truncated === 1,
|
|
747
|
+
responseStatus: row.response_status ?? undefined,
|
|
748
|
+
responseHeaders: row.response_headers
|
|
749
|
+
? this.safeParseHeaders(row.response_headers)
|
|
750
|
+
: undefined,
|
|
751
|
+
responseBody: row.response_body ?? undefined,
|
|
752
|
+
responseBodyTruncated: row.response_body_truncated === 1,
|
|
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,
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
//# sourceMappingURL=storage.js.map
|