@caido-utils/backend 1.3.0 → 1.5.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/README.md ADDED
@@ -0,0 +1,225 @@
1
+ # @caido-utils/backend
2
+
3
+ Backend utilities for Caido plugins.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @caido-utils/backend
9
+ ```
10
+
11
+ **Peer dependency:** `@caido/sdk-backend`
12
+
13
+ ```ts
14
+ import { queryRequests, resolveFilterQuery, Queue } from "@caido-utils/backend";
15
+ ```
16
+
17
+ ---
18
+
19
+ ## Requests
20
+
21
+ ### queryRequests
22
+
23
+ Paginated query over all requests with optional filtering, deduplication, and scope checking.
24
+
25
+ ```ts
26
+ const items = await queryRequests(sdk, {
27
+ filter: 'req.host.cont:"example.com"',
28
+ excludeStaticAssets: true,
29
+ deduplicate: true,
30
+ inScope: true,
31
+ onPage: (pageItems) => console.log(`Got ${pageItems.length} items`),
32
+ });
33
+ ```
34
+
35
+ #### Options
36
+
37
+ | Option | Type | Default | Description |
38
+ |--------|------|---------|-------------|
39
+ | `filter` | `string` | - | HTTPQL filter string |
40
+ | `pageSize` | `number` | `1000` | Items per page |
41
+ | `excludeStaticAssets` | `boolean` | `false` | Filter out images, fonts, JS, CSS, media |
42
+ | `deduplicate` | `boolean` | `false` | Keep last occurrence per `method\|host\|path` |
43
+ | `inScope` | `boolean` | `false` | Post-filter using `sdk.requests.inScope()` |
44
+ | `onPage` | `(items[]) => void` | - | Callback after each page |
45
+
46
+ Always applies base filter `resp.roundtrip.gt:0` (only requests with responses).
47
+
48
+ ---
49
+
50
+ ### Fetch
51
+
52
+ Concurrent HTTP request sender with configurable parallelism and delay.
53
+
54
+ ```ts
55
+ const fetcher = new Fetch(sdk, { threads: 5, delay: 100 });
56
+ const response = await fetcher.send(requestSpec);
57
+ ```
58
+
59
+ #### Constructor Options
60
+
61
+ | Option | Type | Default | Description |
62
+ |--------|------|---------|-------------|
63
+ | `threads` | `number` | `10` | Max concurrent requests |
64
+ | `delay` | `number` | `0` | Delay in ms after each request completes |
65
+
66
+ #### Methods
67
+
68
+ | Method | Signature | Description |
69
+ |--------|-----------|-------------|
70
+ | `send` | `(request: RequestSpec \| RequestSpecRaw) => Promise<RequestResponse>` | Queue and send a request |
71
+
72
+ ---
73
+
74
+ ## Filter
75
+
76
+ ### resolveFilterQuery
77
+
78
+ Resolves custom `filter:` tokens into real HTTPQL.
79
+
80
+ ```ts
81
+ const resolved = resolveFilterQuery('filter:1hr AND req.host.cont:"example.com"');
82
+ // => 'req.created_at.gt:"2026-02-16 15:00:00" AND req.host.cont:"example.com"'
83
+ ```
84
+
85
+ #### Token Mapping
86
+
87
+ | Token | Expands To |
88
+ |-------|------------|
89
+ | `filter:inscope` | `preset:inscope` |
90
+ | `filter:recent` | `req.created_at.gt:"<15 minutes ago>"` |
91
+ | `filter:1hr` | `req.created_at.gt:"<1 hour ago>"` |
92
+ | `filter:6hr` | `req.created_at.gt:"<6 hours ago>"` |
93
+ | `filter:12hr` | `req.created_at.gt:"<12 hours ago>"` |
94
+ | `filter:24hr` | `req.created_at.gt:"<24 hours ago>"` |
95
+ | `filter:<unknown>` | Removed |
96
+
97
+ Timestamps use local time in `YYYY-MM-DD HH:MM:SS` format.
98
+
99
+ ---
100
+
101
+ ## Pool
102
+
103
+ ### Queue
104
+
105
+ Concurrent async task queue with pause/resume/stop.
106
+
107
+ ```ts
108
+ const queue = new Queue<string>(
109
+ { concurrency: 5, delayMs: 100 },
110
+ async (url) => {
111
+ await processUrl(url);
112
+ },
113
+ {
114
+ onError: (url, error) => console.error(`Failed: ${url}`, error),
115
+ },
116
+ );
117
+
118
+ queue.enqueue(["https://a.com", "https://b.com"]);
119
+ queue.pause();
120
+ queue.resume();
121
+ queue.stop();
122
+ ```
123
+
124
+ #### Config
125
+
126
+ | Option | Type | Default | Description |
127
+ |--------|------|---------|-------------|
128
+ | `concurrency` | `number` | required | Max parallel workers |
129
+ | `delayMs` | `number` | `0` | Delay between items per worker |
130
+
131
+ #### Callbacks
132
+
133
+ | Callback | Signature | Description |
134
+ |----------|-----------|-------------|
135
+ | `onError` | `(item: T, error: string) => void` | Called when a task throws |
136
+
137
+ #### Methods
138
+
139
+ | Method | Signature | Description |
140
+ |--------|-----------|-------------|
141
+ | `enqueue` | `(items: T \| T[]) => void` | Add items to the queue |
142
+ | `pause` | `() => void` | Pause processing |
143
+ | `resume` | `() => void` | Resume processing |
144
+ | `stop` | `() => void` | Stop and clear queue |
145
+ | `isRunning` | `() => boolean` | Check if queue is active |
146
+ | `isPaused` | `() => boolean` | Check if paused |
147
+ | `setConcurrency` | `(n: number) => void` | Change max workers at runtime |
148
+ | `pending` | `number` | Items waiting |
149
+ | `completed` | `number` | Items processed |
150
+
151
+ ---
152
+
153
+ ## Crypto
154
+
155
+ ### sha256
156
+
157
+ ```ts
158
+ const hash = sha256("content");
159
+ // => "ed7002b439e9ac845f22357d822bac..."
160
+ ```
161
+
162
+ Returns hex-encoded SHA-256 hash.
163
+
164
+ ---
165
+
166
+ ## Process
167
+
168
+ ### spawnCommand
169
+
170
+ Run a binary bundled in the plugin's assets directory.
171
+
172
+ ```ts
173
+ const output = await spawnCommand(sdk, "my-tool", ["--flag", "value"], stdinData);
174
+ ```
175
+
176
+ | Arg | Type | Description |
177
+ |-----|------|-------------|
178
+ | `sdk` | `SDK` | Backend SDK instance |
179
+ | `binary` | `string` | Filename in `sdk.meta.assetsPath()` |
180
+ | `args` | `string[]` | Command-line arguments |
181
+ | `stdin` | `string` | Optional stdin input |
182
+
183
+ Returns stdout as a string. Throws on non-zero exit code. Automatically sets executable permissions if needed.
184
+
185
+ ---
186
+
187
+ ## Project
188
+
189
+ ### getProjectId
190
+
191
+ ```ts
192
+ const projectId = await getProjectId(sdk);
193
+ ```
194
+
195
+ Returns the current Caido project ID. Throws `"Project not found"` if no project is active.
196
+
197
+ ---
198
+
199
+ ## Storage
200
+
201
+ File I/O within the plugin's data directory.
202
+
203
+ ### storeFile
204
+
205
+ ```ts
206
+ const filePath = await storeFile(sdk, {
207
+ data: responseBody,
208
+ contentType: "application/json", // determines file extension
209
+ });
210
+ ```
211
+
212
+ | Option | Type | Description |
213
+ |--------|------|-------------|
214
+ | `data` | `string` | File content |
215
+ | `contentType` | `string` | Optional — determines file extension |
216
+
217
+ Returns the absolute file path. Supported extensions: `.js`, `.html`, `.json`, `.xml`, `.css`, `.txt`, `.ts`.
218
+
219
+ ### readFile / deleteFile / fileExists
220
+
221
+ ```ts
222
+ const content = await readFile(filePath);
223
+ await deleteFile(filePath);
224
+ const exists = await fileExists(filePath);
225
+ ```
@@ -0,0 +1 @@
1
+ export { HttpRequest, type HttpRequestOptions } from "./request";
@@ -0,0 +1 @@
1
+ export { HttpRequest } from "./request.js";
@@ -0,0 +1,16 @@
1
+ import type { SDK } from "caido:plugin";
2
+ export type HttpRequestOptions = {
3
+ url: string;
4
+ method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS";
5
+ headers?: Record<string, string> | Array<{
6
+ name: string;
7
+ value: string;
8
+ }>;
9
+ body?: string;
10
+ };
11
+ export declare class HttpRequest {
12
+ private sdk;
13
+ private maxRedirects;
14
+ constructor(sdk: SDK, maxRedirects?: number);
15
+ send(options: HttpRequestOptions): Promise<any>;
16
+ }
@@ -0,0 +1,77 @@
1
+ import { RequestSpec } from "caido:utils";
2
+ import parse from "url-parse";
3
+ const DEFAULT_HEADERS = {
4
+ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36",
5
+ "Sec-Fetch-Site": "same-origin",
6
+ "Sec-Fetch-Mode": "navigate",
7
+ "Sec-Fetch-User": "?1",
8
+ "Sec-Fetch-Dest": "document"
9
+ };
10
+ function normalizeHeaders(headers) {
11
+ if (headers === void 0) {
12
+ return {};
13
+ }
14
+ if (Array.isArray(headers)) {
15
+ return headers.reduce(
16
+ (acc, { name, value }) => {
17
+ acc[name] = value;
18
+ return acc;
19
+ },
20
+ {}
21
+ );
22
+ }
23
+ return headers;
24
+ }
25
+ function resolveRedirectUrl(currentUrl, location) {
26
+ if (location.startsWith("http://") || location.startsWith("https://")) {
27
+ return location;
28
+ }
29
+ const parsed = parse(currentUrl);
30
+ if (location.startsWith("/")) {
31
+ return `${parsed.protocol}//${parsed.host}${location}`;
32
+ }
33
+ const basePath = parsed.pathname.substring(
34
+ 0,
35
+ parsed.pathname.lastIndexOf("/") + 1
36
+ );
37
+ return `${parsed.protocol}//${parsed.host}${basePath}${location}`;
38
+ }
39
+ export class HttpRequest {
40
+ sdk;
41
+ maxRedirects;
42
+ constructor(sdk, maxRedirects = 5) {
43
+ this.sdk = sdk;
44
+ this.maxRedirects = maxRedirects;
45
+ }
46
+ async send(options) {
47
+ let currentUrl = options.url;
48
+ let redirectCount = 0;
49
+ while (redirectCount <= this.maxRedirects) {
50
+ const spec = new RequestSpec(currentUrl);
51
+ if (options.method !== void 0) {
52
+ spec.setMethod(options.method);
53
+ }
54
+ const normalizedHeaders = normalizeHeaders(options.headers);
55
+ const headers = { ...DEFAULT_HEADERS, ...normalizedHeaders };
56
+ for (const [name, value] of Object.entries(headers)) {
57
+ spec.setHeader(name, value);
58
+ }
59
+ if (options.body !== void 0) {
60
+ spec.setBody(options.body);
61
+ }
62
+ const result = await this.sdk.requests.send(spec);
63
+ const statusCode = result.response.getCode();
64
+ if ([301, 302, 303, 307, 308].includes(statusCode)) {
65
+ const locationHeader = result.response.getHeader("Location");
66
+ if (!locationHeader) {
67
+ return result;
68
+ }
69
+ currentUrl = resolveRedirectUrl(currentUrl, locationHeader.toString());
70
+ redirectCount++;
71
+ continue;
72
+ }
73
+ return result;
74
+ }
75
+ throw new Error(`Too many redirects (max: ${this.maxRedirects})`);
76
+ }
77
+ }
package/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
+ export { spawnCommand } from "@caido-utils/shared";
1
2
  export { sha256 } from "./crypto";
2
3
  export { resolveFilterQuery } from "./filter";
4
+ export { HttpRequest, type HttpRequestOptions } from "./http";
3
5
  export { Queue } from "./pool";
4
6
  export type { PoolCallbacks, PoolConfig } from "./pool";
5
- export { spawnCommand } from "./process";
6
7
  export { getProjectId } from "./project";
7
- export { Fetch, queryRequests, type FetchOptions, type QueryRequestsOptions, } from "./requests";
8
+ export { Fetch, filterAuthHeaders, queryRequests, type FetchOptions, type QueryRequestsOptions, } from "./requests";
8
9
  export { deleteFile, fileExists, readFile, storeFile } from "./storage";
package/dist/index.js CHANGED
@@ -1,10 +1,12 @@
1
+ export { spawnCommand } from "@caido-utils/shared";
1
2
  export { sha256 } from "./crypto/index.js";
2
3
  export { resolveFilterQuery } from "./filter/index.js";
4
+ export { HttpRequest } from "./http/index.js";
3
5
  export { Queue } from "./pool/index.js";
4
- export { spawnCommand } from "./process/index.js";
5
6
  export { getProjectId } from "./project/index.js";
6
7
  export {
7
8
  Fetch,
9
+ filterAuthHeaders,
8
10
  queryRequests
9
11
  } from "./requests/index.js";
10
12
  export { deleteFile, fileExists, readFile, storeFile } from "./storage/index.js";
@@ -0,0 +1,6 @@
1
+ type Header = {
2
+ name: string;
3
+ value: string;
4
+ };
5
+ export declare function filterAuthHeaders(headers: Header[]): string[];
6
+ export {};
@@ -0,0 +1,17 @@
1
+ const AUTH_HEADERS = [
2
+ "authorization",
3
+ "x-api-key",
4
+ "x-auth-token",
5
+ "x-access-token",
6
+ "x-csrf-token",
7
+ "x-xsrf-token"
8
+ ];
9
+ export function filterAuthHeaders(headers) {
10
+ const result = [];
11
+ for (const { name, value } of headers) {
12
+ if (AUTH_HEADERS.includes(name.toLowerCase())) {
13
+ result.push(`${name}: ${value}`);
14
+ }
15
+ }
16
+ return result;
17
+ }
@@ -1,2 +1,3 @@
1
1
  export { Fetch, type FetchOptions } from "./fetcher";
2
+ export { filterAuthHeaders } from "./headers";
2
3
  export { queryRequests, type QueryRequestsOptions } from "./query";
@@ -1,2 +1,3 @@
1
1
  export { Fetch } from "./fetcher.js";
2
+ export { filterAuthHeaders } from "./headers.js";
2
3
  export { queryRequests } from "./query.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caido-utils/backend",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"
@@ -14,6 +14,9 @@
14
14
  "scripts": {
15
15
  "build": "unbuild"
16
16
  },
17
+ "dependencies": {
18
+ "@caido-utils/shared": "workspace:*"
19
+ },
17
20
  "peerDependencies": {
18
21
  "@caido/sdk-backend": ">=0.46.0"
19
22
  },
@@ -1 +0,0 @@
1
- export { spawnCommand } from "./spawn";
@@ -1 +0,0 @@
1
- export { spawnCommand } from "./spawn.js";
@@ -1,2 +0,0 @@
1
- import type { SDK } from "caido:plugin";
2
- export declare function spawnCommand(sdk: SDK, binary: string, args: string[], stdin?: string): Promise<string>;
@@ -1,58 +0,0 @@
1
- import {
2
- spawn as nodeSpawn
3
- } from "child_process";
4
- import { access, constants } from "fs/promises";
5
- import * as path from "path";
6
- async function ensureBinaryExecutable(binaryPath, sdk) {
7
- try {
8
- await access(binaryPath, constants.F_OK | constants.X_OK);
9
- } catch {
10
- try {
11
- await makeExecutable(binaryPath);
12
- } catch (chmodError) {
13
- sdk.console.error(
14
- `Failed to make ${binaryPath} executable: ${chmodError}`
15
- );
16
- throw new Error(`Cannot make binary executable: ${chmodError}`);
17
- }
18
- }
19
- }
20
- async function makeExecutable(binaryPath) {
21
- const child = nodeSpawn("chmod", ["+x", binaryPath], {
22
- shell: true
23
- });
24
- await driveChild(child);
25
- }
26
- export async function spawnCommand(sdk, binary, args, stdin) {
27
- try {
28
- const binaryPath = path.join(sdk.meta.assetsPath(), binary);
29
- await ensureBinaryExecutable(binaryPath, sdk);
30
- const child = nodeSpawn(binaryPath, args);
31
- if (stdin !== void 0) {
32
- child.stdin.write(stdin);
33
- child.stdin.end();
34
- }
35
- const output = await driveChild(child);
36
- return output;
37
- } catch (error) {
38
- sdk.console.error(`Spawn command error (${binary}): ${error}`);
39
- throw error;
40
- }
41
- }
42
- async function driveChild(child, allowExitCode1 = false) {
43
- let output = "";
44
- child.stdout.on("data", (data) => {
45
- output += data.toString();
46
- });
47
- let error = "";
48
- child.stderr.on("data", (data) => {
49
- error += data.toString();
50
- });
51
- const exitCode = await new Promise((resolve) => {
52
- child.on("close", resolve);
53
- });
54
- if (exitCode !== void 0 && exitCode !== 0 && !(allowExitCode1 && exitCode === 1)) {
55
- throw new Error(`subprocess error exit ${exitCode}, ${error}`);
56
- }
57
- return output;
58
- }