@gpzhang2001/sharpkit-proxy 0.2.1
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 +201 -0
- package/README.md +27 -0
- package/THIRD_PARTY_NOTICES.md +48 -0
- package/lib/index.d.ts +110 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +1307 -0
- package/lib/index.js.map +1 -0
- package/package.json +47 -0
- package/src/client.ts +436 -0
- package/src/index.ts +598 -0
- package/src/replay.ts +228 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,1307 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
3
|
+
import { Buffer as Buffer$1 } from "node:buffer";
|
|
4
|
+
//#region src/replay.ts
|
|
5
|
+
/** strix clips replay response bodies at 8192 chars (caido_api.py:222). */
|
|
6
|
+
const RESPONSE_BODY_MAX_CHARS = 8192;
|
|
7
|
+
/** Framing headers that must never survive into a modified replay (strix :175). */
|
|
8
|
+
const FRAMING_HEADERS = /* @__PURE__ */ new Set(["content-length", "transfer-encoding"]);
|
|
9
|
+
/**
|
|
10
|
+
* Parse a raw HTTP request text into components (strix `parse_raw_request`).
|
|
11
|
+
* @param rawContent - the raw request text.
|
|
12
|
+
* @returns the parsed components.
|
|
13
|
+
* @throws when the request line is malformed.
|
|
14
|
+
*/
|
|
15
|
+
function parseRawRequest(rawContent) {
|
|
16
|
+
const lines = rawContent.split("\n");
|
|
17
|
+
const requestLine = (lines[0] ?? "").trim().split(" ");
|
|
18
|
+
if (requestLine.length < 2) throw new Error("Invalid request line format");
|
|
19
|
+
const headers = {};
|
|
20
|
+
let bodyStart = 0;
|
|
21
|
+
for (let index = 1; index < lines.length; index++) {
|
|
22
|
+
const line = lines[index] ?? "";
|
|
23
|
+
if (line.trim() === "") {
|
|
24
|
+
bodyStart = index + 1;
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
const separator = line.indexOf(":");
|
|
28
|
+
if (separator !== -1) headers[line.slice(0, separator).trim()] = line.slice(separator + 1).trim();
|
|
29
|
+
}
|
|
30
|
+
const body = bodyStart < lines.length ? lines.slice(bodyStart).join("\n").trim() : "";
|
|
31
|
+
return {
|
|
32
|
+
method: requestLine[0] ?? "",
|
|
33
|
+
urlPath: requestLine[1] ?? "",
|
|
34
|
+
headers,
|
|
35
|
+
body
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Compose the full URL from the original request's connection facts, the
|
|
40
|
+
* parsed components, and an optional explicit url override (strix
|
|
41
|
+
* `full_url_from_components`).
|
|
42
|
+
* @param original - the stored request's host/tls facts.
|
|
43
|
+
* @param components - parsed raw request components.
|
|
44
|
+
* @param modifications - the patch dict.
|
|
45
|
+
* @returns the absolute target URL.
|
|
46
|
+
*/
|
|
47
|
+
function fullUrlFromComponents(original, components, modifications) {
|
|
48
|
+
const override = modifications["url"];
|
|
49
|
+
if (typeof override === "string" && override !== "") return override;
|
|
50
|
+
const hostHeader = components.headers["Host"] ?? original.host;
|
|
51
|
+
return `${original.tls ? "https" : "http"}://${hostHeader}${components.urlPath}`;
|
|
52
|
+
}
|
|
53
|
+
/** Parse a query string into a first-value map (Node parity of parse_qs). */
|
|
54
|
+
function parseQuery(query) {
|
|
55
|
+
const params = {};
|
|
56
|
+
for (const pair of query.split("&")) {
|
|
57
|
+
if (pair === "") continue;
|
|
58
|
+
const eq = pair.indexOf("=");
|
|
59
|
+
const key = eq === -1 ? pair : pair.slice(0, eq);
|
|
60
|
+
const value = eq === -1 ? "" : pair.slice(eq + 1);
|
|
61
|
+
params[decodeURIComponent(key)] = decodeURIComponent(value);
|
|
62
|
+
}
|
|
63
|
+
return params;
|
|
64
|
+
}
|
|
65
|
+
/** Serialize a query map back to a string (Node parity of urlencode). */
|
|
66
|
+
function encodeQuery(params) {
|
|
67
|
+
return Object.entries(params).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Overlay the patch dict onto parsed components (strix `apply_modifications`):
|
|
71
|
+
* params merge into the query string, headers/cookies merge, body replaces.
|
|
72
|
+
* @param components - parsed raw request components.
|
|
73
|
+
* @param modifications - the patch dict (url/params/headers/body/cookies).
|
|
74
|
+
* @param fullUrl - the composed final URL.
|
|
75
|
+
* @returns the modified components keyed like strix.
|
|
76
|
+
*/
|
|
77
|
+
function applyModifications(components, modifications, fullUrl) {
|
|
78
|
+
const headers = { ...components.headers };
|
|
79
|
+
let body = components.body;
|
|
80
|
+
let finalUrl = fullUrl;
|
|
81
|
+
const params = modifications["params"];
|
|
82
|
+
if (params !== void 0 && params !== null && typeof params === "object") {
|
|
83
|
+
const question = finalUrl.indexOf("?");
|
|
84
|
+
const existing = question === -1 ? {} : parseQuery(finalUrl.slice(question + 1));
|
|
85
|
+
for (const [key, value] of Object.entries(params)) existing[key] = String(value);
|
|
86
|
+
finalUrl = `${question === -1 ? finalUrl : finalUrl.slice(0, question)}?${encodeQuery(existing)}`;
|
|
87
|
+
}
|
|
88
|
+
const headerPatch = modifications["headers"];
|
|
89
|
+
if (headerPatch !== void 0 && headerPatch !== null && typeof headerPatch === "object") for (const [key, value] of Object.entries(headerPatch)) headers[key] = String(value);
|
|
90
|
+
const bodyPatch = modifications["body"];
|
|
91
|
+
if (typeof bodyPatch === "string") body = bodyPatch;
|
|
92
|
+
const cookiePatch = modifications["cookies"];
|
|
93
|
+
if (cookiePatch !== void 0 && cookiePatch !== null && typeof cookiePatch === "object") {
|
|
94
|
+
const cookies = {};
|
|
95
|
+
const existingCookie = headers["Cookie"];
|
|
96
|
+
if (existingCookie !== void 0) for (const cookie of existingCookie.split(";")) {
|
|
97
|
+
const eq = cookie.indexOf("=");
|
|
98
|
+
if (eq !== -1) cookies[cookie.slice(0, eq).trim()] = cookie.slice(eq + 1).trim();
|
|
99
|
+
}
|
|
100
|
+
for (const [key, value] of Object.entries(cookiePatch)) cookies[key] = String(value);
|
|
101
|
+
headers["Cookie"] = Object.entries(cookies).map(([key, value]) => `${key}=${value}`).join("; ");
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
method: components.method,
|
|
105
|
+
url: finalUrl,
|
|
106
|
+
headers,
|
|
107
|
+
body
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Rebuild a raw HTTP/1.1 request with connection facts (strix
|
|
112
|
+
* `build_raw_request`): Host/UA defaults, framing headers dropped,
|
|
113
|
+
* Content-Length recomputed from the actual body.
|
|
114
|
+
* @param parts - method/url/headers/body of the replay.
|
|
115
|
+
* @returns the connection target and encoded raw request bytes.
|
|
116
|
+
*/
|
|
117
|
+
function buildRawRequest(parts) {
|
|
118
|
+
let parsed;
|
|
119
|
+
try {
|
|
120
|
+
parsed = new URL(parts.url);
|
|
121
|
+
} catch {
|
|
122
|
+
throw new Error(`Invalid URL: ${parts.url}`);
|
|
123
|
+
}
|
|
124
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error(`Invalid URL: ${parts.url}`);
|
|
125
|
+
const tls = parsed.protocol === "https:";
|
|
126
|
+
const port = parsed.port !== "" ? Number(parsed.port) : tls ? 443 : 80;
|
|
127
|
+
const path = `${parsed.pathname}${parsed.search}`;
|
|
128
|
+
const headers = { ...parts.headers };
|
|
129
|
+
if (headers["Host"] === void 0) headers["Host"] = parsed.host;
|
|
130
|
+
if (headers["User-Agent"] === void 0) headers["User-Agent"] = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36";
|
|
131
|
+
for (const key of Object.keys(headers)) if (FRAMING_HEADERS.has(key.toLowerCase())) delete headers[key];
|
|
132
|
+
if (parts.body !== "") headers["Content-Length"] = String(Buffer.byteLength(parts.body, "utf8"));
|
|
133
|
+
const lines = [`${parts.method.toUpperCase()} ${path} HTTP/1.1`];
|
|
134
|
+
for (const [key, value] of Object.entries(headers)) lines.push(`${key}: ${value}`);
|
|
135
|
+
const raw = Buffer.from(`${lines.join("\r\n")}\r\n\r\n${parts.body}`, "utf8");
|
|
136
|
+
return {
|
|
137
|
+
connection: {
|
|
138
|
+
host: parsed.hostname,
|
|
139
|
+
port,
|
|
140
|
+
tls
|
|
141
|
+
},
|
|
142
|
+
raw
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Parse a raw HTTP response into the list_requests summary shape (strix
|
|
147
|
+
* `parse_raw_response`); null when missing/unparseable. Body clipped at 8192
|
|
148
|
+
* chars with a truncation flag.
|
|
149
|
+
* @param rawBytes - the raw response bytes.
|
|
150
|
+
* @returns the parsed parts, or null.
|
|
151
|
+
*/
|
|
152
|
+
function parseRawResponse(rawBytes) {
|
|
153
|
+
if (rawBytes === null || rawBytes === void 0 || rawBytes.byteLength === 0) return null;
|
|
154
|
+
const text = Buffer.from(rawBytes).toString("latin1");
|
|
155
|
+
const separator = text.indexOf("\r\n\r\n");
|
|
156
|
+
if (separator === -1) return null;
|
|
157
|
+
const lines = text.slice(0, separator).split("\r\n");
|
|
158
|
+
const statusParts = (lines[0] ?? "").split(" ");
|
|
159
|
+
if (statusParts.length < 2 || !/^\d+$/.test(statusParts[1] ?? "")) return null;
|
|
160
|
+
const headers = {};
|
|
161
|
+
for (const line of lines.slice(1)) {
|
|
162
|
+
const colon = line.indexOf(":");
|
|
163
|
+
if (colon === -1) continue;
|
|
164
|
+
headers[line.slice(0, colon).trim()] = line.slice(colon + 1).trim();
|
|
165
|
+
}
|
|
166
|
+
const bodyBytes = Buffer.from(text.slice(separator + 4), "latin1");
|
|
167
|
+
let body = bodyBytes.toString("utf8");
|
|
168
|
+
const truncated = body.length > RESPONSE_BODY_MAX_CHARS;
|
|
169
|
+
if (truncated) body = body.slice(0, RESPONSE_BODY_MAX_CHARS);
|
|
170
|
+
return {
|
|
171
|
+
statusCode: Number(statusParts[1]),
|
|
172
|
+
length: bodyBytes.byteLength,
|
|
173
|
+
headers,
|
|
174
|
+
body,
|
|
175
|
+
bodyTruncated: truncated
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/client.ts
|
|
180
|
+
/**
|
|
181
|
+
* Caido HTTP/GraphQL client — TS port of the CLIENT half of strix
|
|
182
|
+
* tools/proxy/caido_api.py against the HOST-side published endpoint (global
|
|
183
|
+
* fetch + Bearer token from the sandbox session's bootstrap). Documents are
|
|
184
|
+
* trimmed, schema-valid subsets of the caido_sdk_client generated operations
|
|
185
|
+
* (Requests/Request/ReplayEntry/StartReplayTask/sitemap trio). The replay
|
|
186
|
+
* flow adapts the SDK's subscription wait into bounded polling of the entry
|
|
187
|
+
* (finished when a response or an error appears; 30s strix dispatch budget).
|
|
188
|
+
* @module @gpzhang2001/sharpkit-proxy/client
|
|
189
|
+
*/
|
|
190
|
+
/** strix `_REQ_FIELD_MAP` resolved to Caido's RequestResponseOrderBy enums. */
|
|
191
|
+
const SORT_ENUMS = {
|
|
192
|
+
timestamp: "CREATED_AT",
|
|
193
|
+
host: "HOST",
|
|
194
|
+
method: "METHOD",
|
|
195
|
+
path: "PATH",
|
|
196
|
+
source: "SOURCE",
|
|
197
|
+
status_code: "RESP_STATUS_CODE",
|
|
198
|
+
response_time: "RESP_ROUNDTRIP_TIME",
|
|
199
|
+
response_size: "RESP_LENGTH"
|
|
200
|
+
};
|
|
201
|
+
/** Compact Requests query (subset of the SDK's generated document). */
|
|
202
|
+
const REQUESTS_DOC = `query Requests($first: Int, $after: String, $filter: HTTPQLInput, $order: RequestResponseOrderInput, $scopeId: ID, $includeRequestRaw: Boolean!, $includeResponseRaw: Boolean!) {
|
|
203
|
+
requests(first: $first, after: $after, filter: $filter, order: $order, scopeId: $scopeId) {
|
|
204
|
+
edges { cursor node {
|
|
205
|
+
id host port method path query isTls createdAt
|
|
206
|
+
raw @include(if: $includeRequestRaw)
|
|
207
|
+
response { id statusCode roundtripTime length createdAt raw @include(if: $includeResponseRaw) }
|
|
208
|
+
} }
|
|
209
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
210
|
+
}
|
|
211
|
+
}`;
|
|
212
|
+
/** Compact Request query (both raws always requested — SDK parity note). */
|
|
213
|
+
const REQUEST_DOC = `query Request($id: ID!, $includeRequestRaw: Boolean!, $includeResponseRaw: Boolean!) {
|
|
214
|
+
request(id: $id) {
|
|
215
|
+
id host port method path query isTls createdAt
|
|
216
|
+
raw @include(if: $includeRequestRaw)
|
|
217
|
+
response { id statusCode roundtripTime length createdAt raw @include(if: $includeResponseRaw) }
|
|
218
|
+
}
|
|
219
|
+
}`;
|
|
220
|
+
/** Empty replay session create (avoids the double history row the raw-create seeds). */
|
|
221
|
+
const CREATE_REPLAY_SESSION_DOC = `mutation CreateReplaySession($input: CreateReplaySessionInput!) {
|
|
222
|
+
createReplaySession(input: $input) { session { id } error { __typename } }
|
|
223
|
+
}`;
|
|
224
|
+
/** Replay dispatch (strix `replay_send_raw` via the SDK's StartReplayTask). */
|
|
225
|
+
const START_REPLAY_TASK_DOC = `mutation StartReplayTask($sessionId: ID!, $input: StartReplayTaskInput!) {
|
|
226
|
+
startReplayTask(sessionId: $sessionId, input: $input) { error { __typename } task { id replayEntry { id } } }
|
|
227
|
+
}`;
|
|
228
|
+
/** Replay entry poll — finished when request.response or error appears. */
|
|
229
|
+
const REPLAY_ENTRY_DOC = `query ReplayEntry($id: ID!, $includeReplayRaw: Boolean!, $includeRequestRaw: Boolean!, $includeResponseRaw: Boolean!) {
|
|
230
|
+
replayEntry(id: $id) {
|
|
231
|
+
id error
|
|
232
|
+
request { id method path response { id statusCode length roundtripTime raw @include(if: $includeResponseRaw) } }
|
|
233
|
+
}
|
|
234
|
+
}`;
|
|
235
|
+
/** Scope management (strix caido_api.py scope_* via the SDK's ScopeFull set). */
|
|
236
|
+
const SCOPES_DOC = `query Scopes { scopes { id name allowlist denylist indexed } }`;
|
|
237
|
+
const SCOPE_DOC = `query Scope($id: ID!) { scope(id: $id) { id name allowlist denylist indexed } }`;
|
|
238
|
+
const CREATE_SCOPE_DOC = `mutation CreateScope($input: CreateScopeInput!) { createScope(input: $input) { error { __typename } scope { id name allowlist denylist indexed } } }`;
|
|
239
|
+
const UPDATE_SCOPE_DOC = `mutation UpdateScope($id: ID!, $input: UpdateScopeInput!) { updateScope(id: $id, input: $input) { error { __typename } scope { id name allowlist denylist indexed } } }`;
|
|
240
|
+
const DELETE_SCOPE_DOC = `mutation DeleteScope($id: ID!) { deleteScope(id: $id) { deletedId } }`;
|
|
241
|
+
/** Sitemap queries (verbatim field sets from strix caido_api.py:555-592). */
|
|
242
|
+
const SITEMAP_ROOTS_DOC = `query GetSitemapRoots($scopeId: ID) {
|
|
243
|
+
sitemapRootEntries(scopeId: $scopeId) {
|
|
244
|
+
edges { node {
|
|
245
|
+
id kind label hasDescendants
|
|
246
|
+
metadata { ... on SitemapEntryMetadataDomain { isTls port } }
|
|
247
|
+
request { method path response { statusCode } }
|
|
248
|
+
} }
|
|
249
|
+
count { value }
|
|
250
|
+
}
|
|
251
|
+
}`;
|
|
252
|
+
const SITEMAP_DESCENDANTS_DOC = `query GetSitemapDescendants($parentId: ID!, $depth: SitemapDescendantsDepth!) {
|
|
253
|
+
sitemapDescendantEntries(parentId: $parentId, depth: $depth) {
|
|
254
|
+
edges { node {
|
|
255
|
+
id kind label hasDescendants
|
|
256
|
+
request { method path response { statusCode } }
|
|
257
|
+
} }
|
|
258
|
+
count { value }
|
|
259
|
+
}
|
|
260
|
+
}`;
|
|
261
|
+
const SITEMAP_ENTRY_DOC = `query GetSitemapEntry($id: ID!) {
|
|
262
|
+
sitemapEntry(id: $id) {
|
|
263
|
+
id kind label hasDescendants
|
|
264
|
+
metadata { ... on SitemapEntryMetadataDomain { isTls port } }
|
|
265
|
+
request { method path response { statusCode length roundtripTime } }
|
|
266
|
+
requests(first: 30, order: {by: CREATED_AT, ordering: DESC}) {
|
|
267
|
+
edges { node { method path response { statusCode length } } }
|
|
268
|
+
count { value }
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}`;
|
|
272
|
+
/** Minimal GraphQL POST with bearer auth and error normalization. */
|
|
273
|
+
async function graphql(options, doc, variables, signal) {
|
|
274
|
+
const response = await (options.fetchFn ?? fetch)(`${options.baseUrl}/graphql`, {
|
|
275
|
+
method: "POST",
|
|
276
|
+
headers: {
|
|
277
|
+
"Content-Type": "application/json",
|
|
278
|
+
Authorization: `Bearer ${options.token}`
|
|
279
|
+
},
|
|
280
|
+
body: JSON.stringify({
|
|
281
|
+
query: doc,
|
|
282
|
+
variables
|
|
283
|
+
}),
|
|
284
|
+
signal
|
|
285
|
+
});
|
|
286
|
+
const text = await response.text();
|
|
287
|
+
if (response.status !== 200) throw new Error(`caido graphql HTTP ${response.status}: ${text.slice(0, 200)}`);
|
|
288
|
+
let payload;
|
|
289
|
+
try {
|
|
290
|
+
payload = JSON.parse(text);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
throw new Error(`caido graphql unparseable response: ${String(error)}`);
|
|
293
|
+
}
|
|
294
|
+
const record = payload;
|
|
295
|
+
if (record.errors !== void 0 && record.errors !== null) throw new Error(`caido graphql errors: ${JSON.stringify(record.errors).slice(0, 300)}`);
|
|
296
|
+
if (typeof record.data !== "object" || record.data === null) throw new Error("caido graphql carried no data");
|
|
297
|
+
return record.data;
|
|
298
|
+
}
|
|
299
|
+
/** Project one raw requests-connection edge (strix list_requests mapping). */
|
|
300
|
+
function projectEdge(edge) {
|
|
301
|
+
const node = edge.node;
|
|
302
|
+
const response = node.response;
|
|
303
|
+
return {
|
|
304
|
+
cursor: String(edge.cursor),
|
|
305
|
+
request: {
|
|
306
|
+
id: String(node.id),
|
|
307
|
+
host: String(node.host),
|
|
308
|
+
port: Number(node.port),
|
|
309
|
+
method: String(node.method),
|
|
310
|
+
path: String(node.path),
|
|
311
|
+
query: node.query === null || node.query === void 0 ? null : String(node.query),
|
|
312
|
+
tls: node.isTls === true,
|
|
313
|
+
createdAt: new Date(Number(node.createdAt)).toISOString()
|
|
314
|
+
},
|
|
315
|
+
response: response === null || response === void 0 ? null : {
|
|
316
|
+
id: String(response.id),
|
|
317
|
+
statusCode: response.statusCode === null || response.statusCode === void 0 ? null : Number(response.statusCode),
|
|
318
|
+
length: response.length === null || response.length === void 0 ? null : Number(response.length),
|
|
319
|
+
createdAt: response.createdAt === null || response.createdAt === void 0 ? null : new Date(Number(response.createdAt)).toISOString()
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* The Caido client: five operations behind the five proxy tools. All methods
|
|
325
|
+
* take an AbortSignal (tool exec.signal parity).
|
|
326
|
+
*/
|
|
327
|
+
var CaidoClient = class {
|
|
328
|
+
options;
|
|
329
|
+
constructor(options) {
|
|
330
|
+
this.options = options;
|
|
331
|
+
}
|
|
332
|
+
/** List captured requests with HTTPQL filter/cursor/sort/scope (strix list_requests_with_client). */
|
|
333
|
+
async listRequests(options, signal) {
|
|
334
|
+
const data = await graphql(this.options, REQUESTS_DOC, {
|
|
335
|
+
first: options.first ?? 50,
|
|
336
|
+
...options.after !== void 0 && options.after !== "" ? { after: options.after } : {},
|
|
337
|
+
...options.httpqlFilter !== void 0 && options.httpqlFilter !== "" ? { filter: { code: options.httpqlFilter } } : {},
|
|
338
|
+
order: {
|
|
339
|
+
by: SORT_ENUMS[options.sortBy ?? "timestamp"],
|
|
340
|
+
ordering: (options.sortOrder ?? "desc") === "asc" ? "ASC" : "DESC"
|
|
341
|
+
},
|
|
342
|
+
...options.scopeId !== void 0 && options.scopeId !== "" ? { scopeId: options.scopeId } : {},
|
|
343
|
+
includeRequestRaw: false,
|
|
344
|
+
includeResponseRaw: false
|
|
345
|
+
}, signal);
|
|
346
|
+
return {
|
|
347
|
+
entries: data.requests.edges.map(projectEdge),
|
|
348
|
+
pageInfo: {
|
|
349
|
+
hasNextPage: data.requests.pageInfo.hasNextPage === true,
|
|
350
|
+
hasPreviousPage: data.requests.pageInfo.hasPreviousPage === true,
|
|
351
|
+
startCursor: data.requests.pageInfo.startCursor === null || data.requests.pageInfo.startCursor === void 0 ? null : String(data.requests.pageInfo.startCursor),
|
|
352
|
+
endCursor: data.requests.pageInfo.endCursor === null || data.requests.pageInfo.endCursor === void 0 ? null : String(data.requests.pageInfo.endCursor)
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
/** Fetch one request with both raw halves (strix get_request_with_client parity: always request both). */
|
|
357
|
+
async getRequest(requestId, signal) {
|
|
358
|
+
const request = (await graphql(this.options, REQUEST_DOC, {
|
|
359
|
+
id: requestId,
|
|
360
|
+
includeRequestRaw: true,
|
|
361
|
+
includeResponseRaw: true
|
|
362
|
+
}, signal)).request;
|
|
363
|
+
if (request === null || request === void 0) return null;
|
|
364
|
+
return {
|
|
365
|
+
id: String(request.id),
|
|
366
|
+
host: String(request.host),
|
|
367
|
+
port: Number(request.port),
|
|
368
|
+
method: String(request.method),
|
|
369
|
+
tls: request.isTls === true,
|
|
370
|
+
requestRaw: request.raw === null || request.raw === void 0 ? null : Buffer$1.from(String(request.raw), "base64").toString("utf8"),
|
|
371
|
+
responseRaw: request.response?.raw === null || request.response?.raw === void 0 ? null : Buffer$1.from(String(request.response.raw), "base64").toString("utf8")
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Replay one stored request with optional field patches (strix
|
|
376
|
+
* `repeat_request` + `replay_send_raw`): fetch raw → parse → patch →
|
|
377
|
+
* rebuild → create empty session → startReplayTask → poll the entry.
|
|
378
|
+
* @param requestId - the stored request id.
|
|
379
|
+
* @param modifications - patch dict (url/params/headers/body/cookies).
|
|
380
|
+
* @param budget - total dispatch deadline (strix 30s) + poll interval.
|
|
381
|
+
*/
|
|
382
|
+
async replayRequest(requestId, modifications, signal, budget) {
|
|
383
|
+
const stored = await this.getRequest(requestId, signal);
|
|
384
|
+
if (stored === null || stored.requestRaw === null) return null;
|
|
385
|
+
const components = parseRawRequest(stored.requestRaw);
|
|
386
|
+
const fullUrl = fullUrlFromComponents({
|
|
387
|
+
host: stored.host,
|
|
388
|
+
tls: stored.tls
|
|
389
|
+
}, components, modifications ?? {});
|
|
390
|
+
const built = buildRawRequest(applyModifications(components, modifications ?? {}, fullUrl));
|
|
391
|
+
const createdSession = (await graphql(this.options, CREATE_REPLAY_SESSION_DOC, { input: {} }, signal)).createReplaySession.session;
|
|
392
|
+
if (createdSession === null || createdSession === void 0) throw new Error("createReplaySession returned no session");
|
|
393
|
+
const started = Date.now();
|
|
394
|
+
const start = await graphql(this.options, START_REPLAY_TASK_DOC, {
|
|
395
|
+
sessionId: createdSession.id,
|
|
396
|
+
input: {
|
|
397
|
+
connection: {
|
|
398
|
+
host: built.connection.host,
|
|
399
|
+
port: built.connection.port,
|
|
400
|
+
isTLS: built.connection.tls,
|
|
401
|
+
SNI: null
|
|
402
|
+
},
|
|
403
|
+
raw: Buffer$1.from(built.raw).toString("base64"),
|
|
404
|
+
settings: {
|
|
405
|
+
connectionClose: false,
|
|
406
|
+
updateContentLength: true,
|
|
407
|
+
placeholders: []
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}, signal);
|
|
411
|
+
const task = start.startReplayTask.task;
|
|
412
|
+
if (start.startReplayTask.error !== null && start.startReplayTask.error !== void 0) throw new Error(`startReplayTask failed: ${JSON.stringify(start.startReplayTask.error).slice(0, 200)}`);
|
|
413
|
+
if (task === null || task === void 0 || task.replayEntry === null || task.replayEntry === void 0) throw new Error("startReplayTask returned no task/entry");
|
|
414
|
+
for (;;) {
|
|
415
|
+
const node = (await graphql(this.options, REPLAY_ENTRY_DOC, {
|
|
416
|
+
id: task.replayEntry.id,
|
|
417
|
+
includeReplayRaw: false,
|
|
418
|
+
includeRequestRaw: false,
|
|
419
|
+
includeResponseRaw: true
|
|
420
|
+
}, signal)).replayEntry;
|
|
421
|
+
if (node !== null && node !== void 0) {
|
|
422
|
+
const errorText = node.error === null || node.error === void 0 ? void 0 : String(node.error);
|
|
423
|
+
const responseNode = node.request?.response ?? null;
|
|
424
|
+
if (responseNode !== null && responseNode !== void 0) {
|
|
425
|
+
const rawBase64 = responseNode.raw;
|
|
426
|
+
const rawBytes = rawBase64 === null || rawBase64 === void 0 ? null : Buffer$1.from(String(rawBase64), "base64");
|
|
427
|
+
return {
|
|
428
|
+
sessionId: createdSession.id,
|
|
429
|
+
status: "DONE",
|
|
430
|
+
elapsedMs: Date.now() - started,
|
|
431
|
+
...errorText !== void 0 ? { error: errorText } : {},
|
|
432
|
+
response: parseRawResponse(rawBytes)
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
if (errorText !== void 0) return {
|
|
436
|
+
sessionId: createdSession.id,
|
|
437
|
+
status: "ERROR",
|
|
438
|
+
elapsedMs: Date.now() - started,
|
|
439
|
+
error: errorText,
|
|
440
|
+
response: null
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
if (Date.now() - started > budget.dispatchTimeoutMs) return {
|
|
444
|
+
sessionId: createdSession.id,
|
|
445
|
+
status: "ERROR",
|
|
446
|
+
elapsedMs: Date.now() - started,
|
|
447
|
+
error: `Caido replay dispatch did not complete within ${String(Math.round(budget.dispatchTimeoutMs / 1e3))}s — the target may be unroutable from the sandbox, or Caido's outbound HTTP client is stalled; check the target host/port and retry`,
|
|
448
|
+
response: null
|
|
449
|
+
};
|
|
450
|
+
await new Promise((resolve) => setTimeout(resolve, budget.pollIntervalMs));
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
/** Sitemap roots or descendants (strix list_sitemap_with_client). */
|
|
454
|
+
async listSitemap(options, signal) {
|
|
455
|
+
if (options.parentId !== void 0 && options.parentId !== "") return graphql(this.options, SITEMAP_DESCENDANTS_DOC, {
|
|
456
|
+
parentId: options.parentId,
|
|
457
|
+
depth: options.depth ?? "DIRECT"
|
|
458
|
+
}, signal);
|
|
459
|
+
return graphql(this.options, SITEMAP_ROOTS_DOC, options.scopeId !== void 0 && options.scopeId !== "" ? { scopeId: options.scopeId } : {}, signal);
|
|
460
|
+
}
|
|
461
|
+
/** One sitemap entry with its 30 most recent requests (strix view_sitemap_entry_with_client). */
|
|
462
|
+
async viewSitemapEntry(entryId, signal) {
|
|
463
|
+
return graphql(this.options, SITEMAP_ENTRY_DOC, { id: entryId }, signal);
|
|
464
|
+
}
|
|
465
|
+
/** All Caido scopes (strix scope_list). */
|
|
466
|
+
async scopeList(signal) {
|
|
467
|
+
return graphql(this.options, SCOPES_DOC, {}, signal);
|
|
468
|
+
}
|
|
469
|
+
/** One scope by id (strix scope_get). */
|
|
470
|
+
async scopeGet(scopeId, signal) {
|
|
471
|
+
return graphql(this.options, SCOPE_DOC, { id: scopeId }, signal);
|
|
472
|
+
}
|
|
473
|
+
/** Create a scope; empty lists allow-all/deny-none (strix scope_create). */
|
|
474
|
+
async scopeCreate(name, allowlist, denylist, signal) {
|
|
475
|
+
return graphql(this.options, CREATE_SCOPE_DOC, { input: {
|
|
476
|
+
name,
|
|
477
|
+
allowlist,
|
|
478
|
+
denylist
|
|
479
|
+
} }, signal);
|
|
480
|
+
}
|
|
481
|
+
/** Update a scope; allow/deny lists FULLY REPLACE the previous values (strix parity). */
|
|
482
|
+
async scopeUpdate(scopeId, name, allowlist, denylist, signal) {
|
|
483
|
+
return graphql(this.options, UPDATE_SCOPE_DOC, {
|
|
484
|
+
id: scopeId,
|
|
485
|
+
input: {
|
|
486
|
+
name,
|
|
487
|
+
allowlist,
|
|
488
|
+
denylist
|
|
489
|
+
}
|
|
490
|
+
}, signal);
|
|
491
|
+
}
|
|
492
|
+
/** Delete a scope (strix scope_delete). */
|
|
493
|
+
async scopeDelete(scopeId, signal) {
|
|
494
|
+
return graphql(this.options, DELETE_SCOPE_DOC, { id: scopeId }, signal);
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
//#endregion
|
|
498
|
+
//#region src/index.ts
|
|
499
|
+
const name = "pentest-tool-proxy";
|
|
500
|
+
const inject = ["tools", "pentestSandbox"];
|
|
501
|
+
const Config = z.object({
|
|
502
|
+
scanId: z.string().default("pentest"),
|
|
503
|
+
listPageSize: z.number().default(50),
|
|
504
|
+
replayTimeoutMs: z.number().default(3e4),
|
|
505
|
+
caidoTimeoutMs: z.number().default(6e4),
|
|
506
|
+
requireApproval: z.boolean().default(true),
|
|
507
|
+
authorizedTargets: z.array(z.string())
|
|
508
|
+
});
|
|
509
|
+
/**
|
|
510
|
+
* Whether a repeat target host is inside the authorized list. Entries may be
|
|
511
|
+
* bare hosts (`example.com`), wildcard subdomains (`*.example.com`), or full
|
|
512
|
+
* URLs (hostname taken); a candidate matches on exact host or a dot-suffix
|
|
513
|
+
* subdomain. An empty list allows everything (fail-open; warned once).
|
|
514
|
+
* @param host - the replay target hostname.
|
|
515
|
+
* @param authorized - the configured target entries.
|
|
516
|
+
*/
|
|
517
|
+
function targetHostAllowed(host, authorized) {
|
|
518
|
+
if (authorized.length === 0) return true;
|
|
519
|
+
const candidate = host.toLowerCase();
|
|
520
|
+
return authorized.some((entry) => {
|
|
521
|
+
let allowed = entry.trim().toLowerCase();
|
|
522
|
+
if (allowed === "") return false;
|
|
523
|
+
if (allowed.includes("://")) try {
|
|
524
|
+
allowed = new URL(allowed).hostname;
|
|
525
|
+
} catch {}
|
|
526
|
+
allowed = allowed.replace(/^\*\./, "").replace(/\/.*$/, "");
|
|
527
|
+
return allowed !== "" && (candidate === allowed || candidate.endsWith(`.${allowed}`));
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Resolve the replay target hostname: an explicit url modification wins;
|
|
532
|
+
* otherwise the stored request's host.
|
|
533
|
+
*/
|
|
534
|
+
function resolveRepeatTargetHost(storedHost, modifications) {
|
|
535
|
+
const override = modifications?.["url"];
|
|
536
|
+
if (typeof override === "string" && override !== "") try {
|
|
537
|
+
return new URL(override).hostname;
|
|
538
|
+
} catch {}
|
|
539
|
+
return storedHost;
|
|
540
|
+
}
|
|
541
|
+
/** Sitemap page size (strix parity: 30 entries per page). */
|
|
542
|
+
const SITEMAP_PAGE_SIZE = 30;
|
|
543
|
+
/**
|
|
544
|
+
* Slice one sitemap connection payload to a 1-indexed page of edges (the
|
|
545
|
+
* total count is preserved). Payloads without a recognized connection pass
|
|
546
|
+
* through untouched.
|
|
547
|
+
*/
|
|
548
|
+
function pageSitemapPayload(payload, page) {
|
|
549
|
+
for (const key of ["sitemapRootEntries", "sitemapDescendantEntries"]) {
|
|
550
|
+
const connection = payload[key];
|
|
551
|
+
if (typeof connection !== "object" || connection === null) continue;
|
|
552
|
+
const edges = connection.edges;
|
|
553
|
+
if (!Array.isArray(edges)) continue;
|
|
554
|
+
const start = Math.max(0, (page - 1) * 30);
|
|
555
|
+
const sliced = edges.slice(start, start + 30);
|
|
556
|
+
return {
|
|
557
|
+
...payload,
|
|
558
|
+
[key]: {
|
|
559
|
+
...connection,
|
|
560
|
+
edges: sliced,
|
|
561
|
+
page,
|
|
562
|
+
page_size: 30,
|
|
563
|
+
has_more: start + 30 < edges.length
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
return payload;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Compact regex hits over raw content (strix `_format_search_hits`).
|
|
571
|
+
* @param content - the raw text.
|
|
572
|
+
* @param pattern - the model-supplied regex.
|
|
573
|
+
* @returns the hits payload, or an error entry when the regex is invalid.
|
|
574
|
+
*/
|
|
575
|
+
function formatSearchHits(content, pattern) {
|
|
576
|
+
let regex;
|
|
577
|
+
try {
|
|
578
|
+
regex = new RegExp(pattern, "g");
|
|
579
|
+
} catch (error) {
|
|
580
|
+
return {
|
|
581
|
+
hits: [],
|
|
582
|
+
totalHits: 0,
|
|
583
|
+
error: `Invalid regex: ${String(error)}`
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
const hits = [];
|
|
587
|
+
for (const match of content.matchAll(regex)) {
|
|
588
|
+
const start = match.index ?? 0;
|
|
589
|
+
const end = start + match[0].length;
|
|
590
|
+
hits.push({
|
|
591
|
+
match: match[0],
|
|
592
|
+
position: start,
|
|
593
|
+
before: content.slice(Math.max(0, start - 40), start),
|
|
594
|
+
after: content.slice(end, end + 40)
|
|
595
|
+
});
|
|
596
|
+
if (hits.length >= 20) break;
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
hits,
|
|
600
|
+
totalHits: hits.length
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Line-paginated raw content page (strix `_format_text_page`).
|
|
605
|
+
* @param content - the raw text.
|
|
606
|
+
* @param page - 1-indexed page number.
|
|
607
|
+
* @param pageSize - lines per page.
|
|
608
|
+
*/
|
|
609
|
+
function formatTextPage(content, page, pageSize) {
|
|
610
|
+
const lines = content.split("\n");
|
|
611
|
+
const start = Math.max(0, (page - 1) * pageSize);
|
|
612
|
+
const end = start + pageSize;
|
|
613
|
+
return {
|
|
614
|
+
content: lines.slice(start, end).join("\n"),
|
|
615
|
+
page,
|
|
616
|
+
pageSize,
|
|
617
|
+
totalLines: lines.length,
|
|
618
|
+
hasMore: end < lines.length
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
/** Strip undefined values so an object satisfies the JsonValue index contract. */
|
|
622
|
+
function structuredToPlain(value) {
|
|
623
|
+
return JSON.parse(JSON.stringify(value));
|
|
624
|
+
}
|
|
625
|
+
/** Unwrap the GraphQL envelope for one scope row. */
|
|
626
|
+
function plainScope(envelope) {
|
|
627
|
+
const record = envelope;
|
|
628
|
+
for (const key of [
|
|
629
|
+
"scope",
|
|
630
|
+
"createScope",
|
|
631
|
+
"updateScope"
|
|
632
|
+
]) {
|
|
633
|
+
const inner = record[key];
|
|
634
|
+
if (typeof inner === "object" && inner !== null) {
|
|
635
|
+
const scope = inner["scope"];
|
|
636
|
+
if (typeof scope === "object" && scope !== null) return scope;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return record;
|
|
640
|
+
}
|
|
641
|
+
/** Unwrap a scopes list envelope. */
|
|
642
|
+
function plainScopes(rows) {
|
|
643
|
+
return rows.filter((row) => typeof row === "object" && row !== null);
|
|
644
|
+
}
|
|
645
|
+
function apply(ctx, config = {}) {
|
|
646
|
+
const scanId = config.scanId ?? "pentest";
|
|
647
|
+
const listPageSize = config.listPageSize ?? 50;
|
|
648
|
+
const replayTimeoutMs = config.replayTimeoutMs ?? 3e4;
|
|
649
|
+
const caidoTimeoutMs = config.caidoTimeoutMs ?? 6e4;
|
|
650
|
+
let clientPromise;
|
|
651
|
+
let warnedNoAllowlist = false;
|
|
652
|
+
/** Authorized targets: Config first, else the preset's scan targets. Empty = fail-open (warned once). */
|
|
653
|
+
const authorizedTargets = () => {
|
|
654
|
+
if (config.authorizedTargets !== void 0) return config.authorizedTargets;
|
|
655
|
+
const values = ctx.get("pentestPreset")?.authorizedTargets?.map((target) => target.value) ?? [];
|
|
656
|
+
if (values.length === 0 && !warnedNoAllowlist) {
|
|
657
|
+
warnedNoAllowlist = true;
|
|
658
|
+
ctx.logger.warn("pentest-proxy: no authorizedTargets configured; repeat_request target enforcement is inactive (fail-open)");
|
|
659
|
+
}
|
|
660
|
+
return values;
|
|
661
|
+
};
|
|
662
|
+
/** Lazy client: resolves the session's Caido bootstrap on first use (strix parity). */
|
|
663
|
+
const client = (signal) => {
|
|
664
|
+
clientPromise ??= (async () => {
|
|
665
|
+
const endpoint = await (await ctx.pentestSandbox.createSession({ scanId })).caidoEndpoint();
|
|
666
|
+
return new CaidoClient({
|
|
667
|
+
baseUrl: endpoint.baseUrl,
|
|
668
|
+
token: endpoint.token
|
|
669
|
+
});
|
|
670
|
+
})().catch((error) => {
|
|
671
|
+
throw new Error(`Caido client not available in run context: ${String(error instanceof Error ? error.message : error)}`);
|
|
672
|
+
});
|
|
673
|
+
const settled = clientPromise;
|
|
674
|
+
if (settled === void 0) throw new Error("unreachable: client promise just assigned");
|
|
675
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
676
|
+
signal.addEventListener("abort", () => reject(/* @__PURE__ */ new Error("caido call aborted")), { once: true });
|
|
677
|
+
const timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`caido call timed out after ${String(caidoTimeoutMs)}ms`)), caidoTimeoutMs);
|
|
678
|
+
settled.then(() => clearTimeout(timer), () => clearTimeout(timer));
|
|
679
|
+
});
|
|
680
|
+
return Promise.race([settled, timeout]);
|
|
681
|
+
};
|
|
682
|
+
if (config.requireApproval !== false) ctx.on("tools/pre-execute", async (exec, next) => {
|
|
683
|
+
if (exec.name === "repeat_request") return {
|
|
684
|
+
kind: "ask",
|
|
685
|
+
reason: "pentest: replay a captured request against the target"
|
|
686
|
+
};
|
|
687
|
+
return next();
|
|
688
|
+
});
|
|
689
|
+
ctx.tools.register(defineTool({
|
|
690
|
+
name: "list_requests",
|
|
691
|
+
description: `List captured HTTP requests from the Caido proxy with HTTPQL filtering. HTTPQL: integer fields (resp.code, req.port, id, roundtrip) use eq/gt/gte/lt/lte/ne e.g. 'resp.code.gte:400'; text fields (req.method, req.host, req.path, req.query, req.ext, req.raw) use regex/cont/eq e.g. 'req.path.cont:"/api/"'; dates use gt/lt with ISO e.g. 'req.created_at.gt:"2024-01-01T00:00:00Z"'; combine with AND/OR; no NOT — use ne/ncont/nregex. String values MUST be quoted, integers MUST NOT. A bare quoted string searches req.raw and resp.raw. Pagination: pass page_info.end_cursor as after.`,
|
|
692
|
+
parameters: {
|
|
693
|
+
httpql_filter: {
|
|
694
|
+
type: "string",
|
|
695
|
+
description: "Caido HTTPQL query (optional)."
|
|
696
|
+
},
|
|
697
|
+
first: {
|
|
698
|
+
type: "integer",
|
|
699
|
+
description: `Entries per page (default ${String(listPageSize)}).`
|
|
700
|
+
},
|
|
701
|
+
after: {
|
|
702
|
+
type: "string",
|
|
703
|
+
description: "Cursor from a previous response's page_info.end_cursor."
|
|
704
|
+
},
|
|
705
|
+
sort_by: {
|
|
706
|
+
type: "string",
|
|
707
|
+
enum: [
|
|
708
|
+
"timestamp",
|
|
709
|
+
"host",
|
|
710
|
+
"method",
|
|
711
|
+
"path",
|
|
712
|
+
"status_code",
|
|
713
|
+
"response_time",
|
|
714
|
+
"response_size",
|
|
715
|
+
"source"
|
|
716
|
+
],
|
|
717
|
+
description: "Sort key (default timestamp)."
|
|
718
|
+
},
|
|
719
|
+
sort_order: {
|
|
720
|
+
type: "string",
|
|
721
|
+
enum: ["asc", "desc"],
|
|
722
|
+
description: "Sort order (default desc)."
|
|
723
|
+
},
|
|
724
|
+
scope_id: {
|
|
725
|
+
type: "string",
|
|
726
|
+
description: "Restrict to a Caido scope."
|
|
727
|
+
}
|
|
728
|
+
},
|
|
729
|
+
output: {
|
|
730
|
+
schema: {
|
|
731
|
+
type: "object",
|
|
732
|
+
properties: {
|
|
733
|
+
success: {
|
|
734
|
+
type: "boolean",
|
|
735
|
+
required: true
|
|
736
|
+
},
|
|
737
|
+
entries: {
|
|
738
|
+
type: "array",
|
|
739
|
+
items: {
|
|
740
|
+
type: "object",
|
|
741
|
+
properties: {},
|
|
742
|
+
additionalProperties: true
|
|
743
|
+
}
|
|
744
|
+
},
|
|
745
|
+
page_info: {
|
|
746
|
+
type: "object",
|
|
747
|
+
properties: {},
|
|
748
|
+
additionalProperties: true
|
|
749
|
+
},
|
|
750
|
+
error: { type: "string" }
|
|
751
|
+
},
|
|
752
|
+
additionalProperties: false
|
|
753
|
+
},
|
|
754
|
+
render: (_args, value) => {
|
|
755
|
+
const result = value;
|
|
756
|
+
if (!result.success) return [{
|
|
757
|
+
type: "text",
|
|
758
|
+
text: `list_requests failed: ${result.error ?? "unknown"}`
|
|
759
|
+
}];
|
|
760
|
+
return [{
|
|
761
|
+
type: "text",
|
|
762
|
+
text: `${String(result.entries.length)} entries (cursor pagination via page_info)`
|
|
763
|
+
}];
|
|
764
|
+
}
|
|
765
|
+
},
|
|
766
|
+
execute: async (args, exec) => {
|
|
767
|
+
try {
|
|
768
|
+
const connection = await (await client(exec.signal)).listRequests({
|
|
769
|
+
httpqlFilter: args.httpql_filter,
|
|
770
|
+
first: args.first ?? listPageSize,
|
|
771
|
+
after: args.after,
|
|
772
|
+
sortBy: args.sort_by,
|
|
773
|
+
sortOrder: args.sort_order,
|
|
774
|
+
scopeId: args.scope_id
|
|
775
|
+
}, exec.signal);
|
|
776
|
+
return {
|
|
777
|
+
success: true,
|
|
778
|
+
entries: connection.entries.map((entry) => ({
|
|
779
|
+
cursor: entry.cursor,
|
|
780
|
+
request: {
|
|
781
|
+
id: entry.request.id,
|
|
782
|
+
host: entry.request.host,
|
|
783
|
+
port: entry.request.port,
|
|
784
|
+
method: entry.request.method,
|
|
785
|
+
path: entry.request.path,
|
|
786
|
+
query: entry.request.query,
|
|
787
|
+
is_tls: entry.request.tls,
|
|
788
|
+
created_at: entry.request.createdAt
|
|
789
|
+
},
|
|
790
|
+
response: entry.response === null ? null : {
|
|
791
|
+
id: entry.response.id,
|
|
792
|
+
status_code: entry.response.statusCode,
|
|
793
|
+
length: entry.response.length,
|
|
794
|
+
created_at: entry.response.createdAt
|
|
795
|
+
}
|
|
796
|
+
})),
|
|
797
|
+
page_info: {
|
|
798
|
+
has_next_page: connection.pageInfo.hasNextPage,
|
|
799
|
+
has_previous_page: connection.pageInfo.hasPreviousPage,
|
|
800
|
+
start_cursor: connection.pageInfo.startCursor,
|
|
801
|
+
end_cursor: connection.pageInfo.endCursor
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
} catch (error) {
|
|
805
|
+
return {
|
|
806
|
+
success: false,
|
|
807
|
+
entries: [],
|
|
808
|
+
page_info: {},
|
|
809
|
+
error: String(error instanceof Error ? error.message : error)
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}));
|
|
814
|
+
ctx.tools.register(defineTool({
|
|
815
|
+
name: "view_request",
|
|
816
|
+
description: `View a captured request or its response raw content, optionally regex-searched. With search_pattern: up to 20 compact hits with before/after context (hunting reflections, leaked URLs, hidden parameters). Without: full content line-paginated (page, page_size). Patterns like '/api/[a-zA-Z0-9._/-]+' (endpoints), 'https?://[^\\s<>"]+' (URLs), '[?&][a-zA-Z0-9_]+=([^&\\s]+)' (query params).`,
|
|
817
|
+
parameters: {
|
|
818
|
+
request_id: {
|
|
819
|
+
type: "string",
|
|
820
|
+
required: true,
|
|
821
|
+
description: "Request ID from list_requests."
|
|
822
|
+
},
|
|
823
|
+
part: {
|
|
824
|
+
type: "string",
|
|
825
|
+
enum: ["request", "response"],
|
|
826
|
+
description: "Which raw half to view (default request)."
|
|
827
|
+
},
|
|
828
|
+
search_pattern: {
|
|
829
|
+
type: "string",
|
|
830
|
+
description: "Optional regex; switches to compact hits mode."
|
|
831
|
+
},
|
|
832
|
+
page: {
|
|
833
|
+
type: "integer",
|
|
834
|
+
description: "1-indexed page (only without search_pattern)."
|
|
835
|
+
},
|
|
836
|
+
page_size: {
|
|
837
|
+
type: "integer",
|
|
838
|
+
description: "Lines per page (default 50)."
|
|
839
|
+
}
|
|
840
|
+
},
|
|
841
|
+
output: {
|
|
842
|
+
schema: {
|
|
843
|
+
type: "object",
|
|
844
|
+
properties: {
|
|
845
|
+
kind: {
|
|
846
|
+
type: "string",
|
|
847
|
+
required: true,
|
|
848
|
+
enum: [
|
|
849
|
+
"hits",
|
|
850
|
+
"page",
|
|
851
|
+
"error"
|
|
852
|
+
]
|
|
853
|
+
},
|
|
854
|
+
hits: {
|
|
855
|
+
type: "array",
|
|
856
|
+
items: {
|
|
857
|
+
type: "object",
|
|
858
|
+
properties: {},
|
|
859
|
+
additionalProperties: true
|
|
860
|
+
}
|
|
861
|
+
},
|
|
862
|
+
total_hits: { type: "integer" },
|
|
863
|
+
content: { type: "string" },
|
|
864
|
+
page: { type: "integer" },
|
|
865
|
+
page_size: { type: "integer" },
|
|
866
|
+
total_lines: { type: "integer" },
|
|
867
|
+
has_more: { type: "boolean" },
|
|
868
|
+
error: { type: "string" }
|
|
869
|
+
},
|
|
870
|
+
additionalProperties: false
|
|
871
|
+
},
|
|
872
|
+
render: (_args, value) => {
|
|
873
|
+
const result = value;
|
|
874
|
+
if (result.kind === "error") return [{
|
|
875
|
+
type: "text",
|
|
876
|
+
text: `view_request failed: ${result.error}`
|
|
877
|
+
}];
|
|
878
|
+
if (result.kind === "hits") return [{
|
|
879
|
+
type: "text",
|
|
880
|
+
text: `${String(result.total_hits)} regex hit(s)`
|
|
881
|
+
}];
|
|
882
|
+
return [{
|
|
883
|
+
type: "text",
|
|
884
|
+
text: `page ${String(result.page)}/${String(Math.ceil(result.total_lines / Math.max(1, result.page_size)))} of ${String(result.total_lines)} lines`
|
|
885
|
+
}];
|
|
886
|
+
}
|
|
887
|
+
},
|
|
888
|
+
execute: async (args, exec) => {
|
|
889
|
+
try {
|
|
890
|
+
const stored = await (await client(exec.signal)).getRequest(args.request_id, exec.signal);
|
|
891
|
+
if (stored === null) return {
|
|
892
|
+
kind: "error",
|
|
893
|
+
error: `Request ${args.request_id} not found`
|
|
894
|
+
};
|
|
895
|
+
const raw = args.part === "response" ? stored.responseRaw : stored.requestRaw;
|
|
896
|
+
if (raw === null) return {
|
|
897
|
+
kind: "error",
|
|
898
|
+
error: `No raw ${args.part === "response" ? "response" : "request"} for ${args.request_id}`
|
|
899
|
+
};
|
|
900
|
+
if (args.search_pattern !== void 0 && args.search_pattern !== "") {
|
|
901
|
+
const hits = formatSearchHits(raw, args.search_pattern);
|
|
902
|
+
if (hits.error !== void 0) return {
|
|
903
|
+
kind: "error",
|
|
904
|
+
error: hits.error
|
|
905
|
+
};
|
|
906
|
+
return {
|
|
907
|
+
kind: "hits",
|
|
908
|
+
hits: hits.hits.map((hit) => ({
|
|
909
|
+
match: hit.match,
|
|
910
|
+
position: hit.position,
|
|
911
|
+
before: hit.before,
|
|
912
|
+
after: hit.after
|
|
913
|
+
})),
|
|
914
|
+
total_hits: hits.totalHits
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
const textPage = formatTextPage(raw, args.page ?? 1, args.page_size ?? 50);
|
|
918
|
+
return {
|
|
919
|
+
kind: "page",
|
|
920
|
+
content: textPage.content,
|
|
921
|
+
page: textPage.page,
|
|
922
|
+
page_size: textPage.pageSize,
|
|
923
|
+
total_lines: textPage.totalLines,
|
|
924
|
+
has_more: textPage.hasMore
|
|
925
|
+
};
|
|
926
|
+
} catch (error) {
|
|
927
|
+
return {
|
|
928
|
+
kind: "error",
|
|
929
|
+
error: String(error instanceof Error ? error.message : error)
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}));
|
|
934
|
+
ctx.tools.register(defineTool({
|
|
935
|
+
name: "repeat_request",
|
|
936
|
+
description: `Repeat a captured request, optionally patching fields — the browse→capture→modify→test flow. modifications keys: url (replace), params (query additions), headers (additions), body (replace), cookies (additions). Inherits everything else from the original.`,
|
|
937
|
+
parameters: {
|
|
938
|
+
request_id: {
|
|
939
|
+
type: "string",
|
|
940
|
+
required: true,
|
|
941
|
+
description: "ID of the original request (from list_requests)."
|
|
942
|
+
},
|
|
943
|
+
modifications: {
|
|
944
|
+
type: "object",
|
|
945
|
+
properties: {
|
|
946
|
+
url: {
|
|
947
|
+
type: "string",
|
|
948
|
+
description: "Replace the URL."
|
|
949
|
+
},
|
|
950
|
+
params: {
|
|
951
|
+
type: "object",
|
|
952
|
+
properties: {},
|
|
953
|
+
additionalProperties: true,
|
|
954
|
+
description: "Query-string keys to add/update."
|
|
955
|
+
},
|
|
956
|
+
headers: {
|
|
957
|
+
type: "object",
|
|
958
|
+
properties: {},
|
|
959
|
+
additionalProperties: true,
|
|
960
|
+
description: "Headers to add/update."
|
|
961
|
+
},
|
|
962
|
+
body: {
|
|
963
|
+
type: "string",
|
|
964
|
+
description: "Replace the body."
|
|
965
|
+
},
|
|
966
|
+
cookies: {
|
|
967
|
+
type: "object",
|
|
968
|
+
properties: {},
|
|
969
|
+
additionalProperties: true,
|
|
970
|
+
description: "Cookies to add/update."
|
|
971
|
+
}
|
|
972
|
+
},
|
|
973
|
+
additionalProperties: false,
|
|
974
|
+
description: "Patch dict overlaying the original request."
|
|
975
|
+
}
|
|
976
|
+
},
|
|
977
|
+
output: {
|
|
978
|
+
schema: {
|
|
979
|
+
type: "object",
|
|
980
|
+
properties: {
|
|
981
|
+
success: {
|
|
982
|
+
type: "boolean",
|
|
983
|
+
required: true
|
|
984
|
+
},
|
|
985
|
+
status: { type: "string" },
|
|
986
|
+
session_id: { type: "string" },
|
|
987
|
+
elapsed_ms: { type: "integer" },
|
|
988
|
+
response: {
|
|
989
|
+
type: "object",
|
|
990
|
+
properties: {},
|
|
991
|
+
additionalProperties: true
|
|
992
|
+
},
|
|
993
|
+
error: { type: "string" }
|
|
994
|
+
},
|
|
995
|
+
additionalProperties: false
|
|
996
|
+
},
|
|
997
|
+
render: (_args, value) => {
|
|
998
|
+
const result = value;
|
|
999
|
+
if (!result.success) return [{
|
|
1000
|
+
type: "text",
|
|
1001
|
+
text: `repeat_request failed: ${result.error ?? result.status ?? "unknown"}`
|
|
1002
|
+
}];
|
|
1003
|
+
return [{
|
|
1004
|
+
type: "text",
|
|
1005
|
+
text: `replay ${result.status ?? "DONE"}${result.response?.status_code === void 0 ? "" : ` → HTTP ${String(result.response.status_code)}`}`
|
|
1006
|
+
}];
|
|
1007
|
+
}
|
|
1008
|
+
},
|
|
1009
|
+
execute: async (args, exec) => {
|
|
1010
|
+
try {
|
|
1011
|
+
const caido = await client(exec.signal);
|
|
1012
|
+
const stored = await caido.getRequest(args.request_id, exec.signal);
|
|
1013
|
+
if (stored === null) return {
|
|
1014
|
+
success: false,
|
|
1015
|
+
status: "ERROR",
|
|
1016
|
+
error: `Request ${args.request_id} not found`
|
|
1017
|
+
};
|
|
1018
|
+
const allowlist = authorizedTargets();
|
|
1019
|
+
const targetHost = resolveRepeatTargetHost(stored.host, args.modifications);
|
|
1020
|
+
if (!targetHostAllowed(targetHost, allowlist)) return {
|
|
1021
|
+
success: false,
|
|
1022
|
+
status: "ERROR",
|
|
1023
|
+
error: `repeat_request refused: target host '${targetHost}' is outside this scan's authorized targets`
|
|
1024
|
+
};
|
|
1025
|
+
const replay = await caido.replayRequest(args.request_id, args.modifications, exec.signal, {
|
|
1026
|
+
dispatchTimeoutMs: replayTimeoutMs,
|
|
1027
|
+
pollIntervalMs: 500
|
|
1028
|
+
});
|
|
1029
|
+
if (replay === null) return {
|
|
1030
|
+
success: false,
|
|
1031
|
+
status: "ERROR",
|
|
1032
|
+
error: `Request ${args.request_id} not found`
|
|
1033
|
+
};
|
|
1034
|
+
return {
|
|
1035
|
+
success: replay.status === "DONE",
|
|
1036
|
+
status: replay.status,
|
|
1037
|
+
session_id: replay.sessionId,
|
|
1038
|
+
elapsed_ms: replay.elapsedMs,
|
|
1039
|
+
response: replay.response === null ? {} : {
|
|
1040
|
+
status_code: replay.response.statusCode,
|
|
1041
|
+
length: replay.response.length,
|
|
1042
|
+
headers: replay.response.headers,
|
|
1043
|
+
body: replay.response.body,
|
|
1044
|
+
body_truncated: replay.response.bodyTruncated
|
|
1045
|
+
},
|
|
1046
|
+
...replay.error !== void 0 ? { error: replay.error } : {}
|
|
1047
|
+
};
|
|
1048
|
+
} catch (error) {
|
|
1049
|
+
return {
|
|
1050
|
+
success: false,
|
|
1051
|
+
status: "ERROR",
|
|
1052
|
+
error: String(error instanceof Error ? error.message : error)
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}));
|
|
1057
|
+
ctx.tools.register(defineTool({
|
|
1058
|
+
name: "list_sitemap",
|
|
1059
|
+
description: `Browse Caido's hierarchical sitemap: DOMAIN → DIRECTORY → REQUEST → REQUEST_BODY/REQUEST_QUERY. Start with no parent_id for root domains (optionally scope-filtered); drill in by passing an entry's id as parent_id (depth DIRECT or ALL). Pair with view_sitemap_entry.`,
|
|
1060
|
+
parameters: {
|
|
1061
|
+
scope_id: {
|
|
1062
|
+
type: "string",
|
|
1063
|
+
description: "Limit roots to a Caido scope (only without parent_id)."
|
|
1064
|
+
},
|
|
1065
|
+
parent_id: {
|
|
1066
|
+
type: "string",
|
|
1067
|
+
description: "Entry ID to expand; omit for root domains."
|
|
1068
|
+
},
|
|
1069
|
+
depth: {
|
|
1070
|
+
type: "string",
|
|
1071
|
+
enum: ["DIRECT", "ALL"],
|
|
1072
|
+
description: "DIRECT children or the full subtree (default DIRECT)."
|
|
1073
|
+
},
|
|
1074
|
+
page: {
|
|
1075
|
+
type: "integer",
|
|
1076
|
+
description: "1-indexed page (30 entries per page, strix parity)."
|
|
1077
|
+
}
|
|
1078
|
+
},
|
|
1079
|
+
output: {
|
|
1080
|
+
schema: {
|
|
1081
|
+
type: "object",
|
|
1082
|
+
properties: {
|
|
1083
|
+
success: {
|
|
1084
|
+
type: "boolean",
|
|
1085
|
+
required: true
|
|
1086
|
+
},
|
|
1087
|
+
payload: {
|
|
1088
|
+
type: "object",
|
|
1089
|
+
properties: {},
|
|
1090
|
+
additionalProperties: true
|
|
1091
|
+
},
|
|
1092
|
+
error: { type: "string" }
|
|
1093
|
+
},
|
|
1094
|
+
additionalProperties: false
|
|
1095
|
+
},
|
|
1096
|
+
render: (_args, value) => {
|
|
1097
|
+
const result = value;
|
|
1098
|
+
return [{
|
|
1099
|
+
type: "text",
|
|
1100
|
+
text: result.success ? "sitemap payload returned" : `list_sitemap failed: ${result.error ?? "unknown"}`
|
|
1101
|
+
}];
|
|
1102
|
+
}
|
|
1103
|
+
},
|
|
1104
|
+
execute: async (args, exec) => {
|
|
1105
|
+
try {
|
|
1106
|
+
return {
|
|
1107
|
+
success: true,
|
|
1108
|
+
payload: structuredToPlain(pageSitemapPayload(await (await client(exec.signal)).listSitemap({
|
|
1109
|
+
scopeId: args.scope_id,
|
|
1110
|
+
parentId: args.parent_id,
|
|
1111
|
+
depth: args.depth
|
|
1112
|
+
}, exec.signal), args.page ?? 1))
|
|
1113
|
+
};
|
|
1114
|
+
} catch (error) {
|
|
1115
|
+
return {
|
|
1116
|
+
success: false,
|
|
1117
|
+
payload: {},
|
|
1118
|
+
error: String(error instanceof Error ? error.message : error)
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}));
|
|
1123
|
+
ctx.tools.register(defineTool({
|
|
1124
|
+
name: "scope_rules",
|
|
1125
|
+
description: "Manage Caido scope rules: get/list/create/update/delete. Glob patterns (*, ?, [abc], [a-z], [^abc]); an EMPTY allowlist ALLOWS ALL; the denylist overrides the allowlist. Scopes feed list_requests' scope_id.",
|
|
1126
|
+
parameters: {
|
|
1127
|
+
action: {
|
|
1128
|
+
type: "string",
|
|
1129
|
+
required: true,
|
|
1130
|
+
enum: [
|
|
1131
|
+
"get",
|
|
1132
|
+
"list",
|
|
1133
|
+
"create",
|
|
1134
|
+
"update",
|
|
1135
|
+
"delete"
|
|
1136
|
+
],
|
|
1137
|
+
description: "Scope action."
|
|
1138
|
+
},
|
|
1139
|
+
allowlist: {
|
|
1140
|
+
type: "array",
|
|
1141
|
+
items: { type: "string" },
|
|
1142
|
+
description: "Allow glob patterns (create/update)."
|
|
1143
|
+
},
|
|
1144
|
+
denylist: {
|
|
1145
|
+
type: "array",
|
|
1146
|
+
items: { type: "string" },
|
|
1147
|
+
description: "Deny glob patterns; overrides the allowlist (create/update)."
|
|
1148
|
+
},
|
|
1149
|
+
scope_id: {
|
|
1150
|
+
type: "string",
|
|
1151
|
+
description: "Target scope id (get/update/delete)."
|
|
1152
|
+
},
|
|
1153
|
+
scope_name: {
|
|
1154
|
+
type: "string",
|
|
1155
|
+
description: "Scope name (create/update)."
|
|
1156
|
+
}
|
|
1157
|
+
},
|
|
1158
|
+
output: {
|
|
1159
|
+
schema: {
|
|
1160
|
+
type: "object",
|
|
1161
|
+
properties: {
|
|
1162
|
+
success: {
|
|
1163
|
+
type: "boolean",
|
|
1164
|
+
required: true
|
|
1165
|
+
},
|
|
1166
|
+
scopes: {
|
|
1167
|
+
type: "array",
|
|
1168
|
+
items: {
|
|
1169
|
+
type: "object",
|
|
1170
|
+
properties: {},
|
|
1171
|
+
additionalProperties: true
|
|
1172
|
+
}
|
|
1173
|
+
},
|
|
1174
|
+
scope: {
|
|
1175
|
+
type: "object",
|
|
1176
|
+
properties: {},
|
|
1177
|
+
additionalProperties: true
|
|
1178
|
+
},
|
|
1179
|
+
deleted: { type: "string" },
|
|
1180
|
+
message: { type: "string" },
|
|
1181
|
+
error: { type: "string" }
|
|
1182
|
+
},
|
|
1183
|
+
additionalProperties: false
|
|
1184
|
+
},
|
|
1185
|
+
render: (_args, value) => {
|
|
1186
|
+
const result = value;
|
|
1187
|
+
return [{
|
|
1188
|
+
type: "text",
|
|
1189
|
+
text: result.success ? "scope action succeeded" : `scope_rules failed: ${result.error ?? "unknown"}`
|
|
1190
|
+
}];
|
|
1191
|
+
}
|
|
1192
|
+
},
|
|
1193
|
+
execute: (async (rawArgs, rawExec) => {
|
|
1194
|
+
const args = rawArgs;
|
|
1195
|
+
const exec = rawExec;
|
|
1196
|
+
try {
|
|
1197
|
+
const caido = await client(exec.signal);
|
|
1198
|
+
const allowlist = args.allowlist ?? [];
|
|
1199
|
+
const denylist = args.denylist ?? [];
|
|
1200
|
+
switch (args.action) {
|
|
1201
|
+
case "list": return {
|
|
1202
|
+
success: true,
|
|
1203
|
+
scopes: plainScopes((await caido.scopeList(exec.signal)).scopes ?? [])
|
|
1204
|
+
};
|
|
1205
|
+
case "get":
|
|
1206
|
+
if (args.scope_id === void 0 || args.scope_id === "") return {
|
|
1207
|
+
success: false,
|
|
1208
|
+
error: "Scope_id is required for action='get'"
|
|
1209
|
+
};
|
|
1210
|
+
return {
|
|
1211
|
+
success: true,
|
|
1212
|
+
scope: plainScope(await caido.scopeGet(args.scope_id, exec.signal))
|
|
1213
|
+
};
|
|
1214
|
+
case "create":
|
|
1215
|
+
if (args.scope_name === void 0 || args.scope_name === "") return {
|
|
1216
|
+
success: false,
|
|
1217
|
+
error: "Scope_name is required for action='create'"
|
|
1218
|
+
};
|
|
1219
|
+
return {
|
|
1220
|
+
success: true,
|
|
1221
|
+
scope: plainScope(await caido.scopeCreate(args.scope_name, allowlist, denylist, exec.signal))
|
|
1222
|
+
};
|
|
1223
|
+
case "update":
|
|
1224
|
+
if (args.scope_id === void 0 || args.scope_id === "" || args.scope_name === void 0 || args.scope_name === "") return {
|
|
1225
|
+
success: false,
|
|
1226
|
+
error: "Scope_id and scope_name are required for action='update'"
|
|
1227
|
+
};
|
|
1228
|
+
return {
|
|
1229
|
+
success: true,
|
|
1230
|
+
scope: plainScope(await caido.scopeUpdate(args.scope_id, args.scope_name, allowlist, denylist, exec.signal))
|
|
1231
|
+
};
|
|
1232
|
+
case "delete":
|
|
1233
|
+
if (args.scope_id === void 0 || args.scope_id === "") return {
|
|
1234
|
+
success: false,
|
|
1235
|
+
error: "Scope_id is required for action='delete'"
|
|
1236
|
+
};
|
|
1237
|
+
return {
|
|
1238
|
+
success: true,
|
|
1239
|
+
deleted: (await caido.scopeDelete(args.scope_id, exec.signal)).deleteScope?.deletedId ?? args.scope_id,
|
|
1240
|
+
message: `Scope ${args.scope_id} deleted`
|
|
1241
|
+
};
|
|
1242
|
+
default: return {
|
|
1243
|
+
success: false,
|
|
1244
|
+
error: `Unknown action: ${String(args.action)}`
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
} catch (error) {
|
|
1248
|
+
return {
|
|
1249
|
+
success: false,
|
|
1250
|
+
error: `scope_rules failed: ${String(error instanceof Error ? error.message : error)}`
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
})
|
|
1254
|
+
}));
|
|
1255
|
+
ctx.tools.register(defineTool({
|
|
1256
|
+
name: "view_sitemap_entry",
|
|
1257
|
+
description: "Full detail for a sitemap entry plus its most recent 30 related requests. Pick entry_id from list_sitemap.",
|
|
1258
|
+
parameters: { entry_id: {
|
|
1259
|
+
type: "string",
|
|
1260
|
+
required: true,
|
|
1261
|
+
description: "ID from list_sitemap (or any nested entry)."
|
|
1262
|
+
} },
|
|
1263
|
+
output: {
|
|
1264
|
+
schema: {
|
|
1265
|
+
type: "object",
|
|
1266
|
+
properties: {
|
|
1267
|
+
success: {
|
|
1268
|
+
type: "boolean",
|
|
1269
|
+
required: true
|
|
1270
|
+
},
|
|
1271
|
+
payload: {
|
|
1272
|
+
type: "object",
|
|
1273
|
+
properties: {},
|
|
1274
|
+
additionalProperties: true
|
|
1275
|
+
},
|
|
1276
|
+
error: { type: "string" }
|
|
1277
|
+
},
|
|
1278
|
+
additionalProperties: false
|
|
1279
|
+
},
|
|
1280
|
+
render: (_args, value) => {
|
|
1281
|
+
const result = value;
|
|
1282
|
+
return [{
|
|
1283
|
+
type: "text",
|
|
1284
|
+
text: result.success ? "sitemap entry payload returned" : `view_sitemap_entry failed: ${result.error ?? "unknown"}`
|
|
1285
|
+
}];
|
|
1286
|
+
}
|
|
1287
|
+
},
|
|
1288
|
+
execute: async (args, exec) => {
|
|
1289
|
+
try {
|
|
1290
|
+
return {
|
|
1291
|
+
success: true,
|
|
1292
|
+
payload: structuredToPlain(await (await client(exec.signal)).viewSitemapEntry(args.entry_id, exec.signal))
|
|
1293
|
+
};
|
|
1294
|
+
} catch (error) {
|
|
1295
|
+
return {
|
|
1296
|
+
success: false,
|
|
1297
|
+
payload: {},
|
|
1298
|
+
error: String(error instanceof Error ? error.message : error)
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
}));
|
|
1303
|
+
}
|
|
1304
|
+
//#endregion
|
|
1305
|
+
export { Config, SITEMAP_PAGE_SIZE, apply, formatSearchHits, formatTextPage, inject, name, pageSitemapPayload, resolveRepeatTargetHost, targetHostAllowed };
|
|
1306
|
+
|
|
1307
|
+
//# sourceMappingURL=index.js.map
|