@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/src/client.ts
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caido HTTP/GraphQL client — TS port of the CLIENT half of strix
|
|
3
|
+
* tools/proxy/caido_api.py against the HOST-side published endpoint (global
|
|
4
|
+
* fetch + Bearer token from the sandbox session's bootstrap). Documents are
|
|
5
|
+
* trimmed, schema-valid subsets of the caido_sdk_client generated operations
|
|
6
|
+
* (Requests/Request/ReplayEntry/StartReplayTask/sitemap trio). The replay
|
|
7
|
+
* flow adapts the SDK's subscription wait into bounded polling of the entry
|
|
8
|
+
* (finished when a response or an error appears; 30s strix dispatch budget).
|
|
9
|
+
* @module @gpzhang2001/sharpkit-proxy/client
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Buffer } from 'node:buffer'
|
|
13
|
+
import { buildRawRequest, fullUrlFromComponents, applyModifications, parseRawRequest, parseRawResponse, type RawResponseParts } from './replay.ts'
|
|
14
|
+
|
|
15
|
+
/** Which half of a captured exchange to surface (strix RequestPart). */
|
|
16
|
+
export type RequestPart = 'request' | 'response'
|
|
17
|
+
|
|
18
|
+
/** Sort keys accepted by list_requests (strix SortBy). */
|
|
19
|
+
export type SortBy = 'timestamp' | 'host' | 'method' | 'path' | 'status_code' | 'response_time' | 'response_size' | 'source'
|
|
20
|
+
|
|
21
|
+
/** strix `_REQ_FIELD_MAP` resolved to Caido's RequestResponseOrderBy enums. */
|
|
22
|
+
const SORT_ENUMS: Record<SortBy, string> = {
|
|
23
|
+
timestamp: 'CREATED_AT',
|
|
24
|
+
host: 'HOST',
|
|
25
|
+
method: 'METHOD',
|
|
26
|
+
path: 'PATH',
|
|
27
|
+
source: 'SOURCE',
|
|
28
|
+
status_code: 'RESP_STATUS_CODE',
|
|
29
|
+
response_time: 'RESP_ROUNDTRIP_TIME',
|
|
30
|
+
response_size: 'RESP_LENGTH',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The one Caido port the sandbox publishes (protocol constant). */
|
|
34
|
+
export const CAIDO_PORT = 48080
|
|
35
|
+
|
|
36
|
+
/** Compact Requests query (subset of the SDK's generated document). */
|
|
37
|
+
const REQUESTS_DOC = `query Requests($first: Int, $after: String, $filter: HTTPQLInput, $order: RequestResponseOrderInput, $scopeId: ID, $includeRequestRaw: Boolean!, $includeResponseRaw: Boolean!) {
|
|
38
|
+
requests(first: $first, after: $after, filter: $filter, order: $order, scopeId: $scopeId) {
|
|
39
|
+
edges { cursor node {
|
|
40
|
+
id host port method path query isTls createdAt
|
|
41
|
+
raw @include(if: $includeRequestRaw)
|
|
42
|
+
response { id statusCode roundtripTime length createdAt raw @include(if: $includeResponseRaw) }
|
|
43
|
+
} }
|
|
44
|
+
pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
|
|
45
|
+
}
|
|
46
|
+
}`
|
|
47
|
+
|
|
48
|
+
/** Compact Request query (both raws always requested — SDK parity note). */
|
|
49
|
+
const REQUEST_DOC = `query Request($id: ID!, $includeRequestRaw: Boolean!, $includeResponseRaw: Boolean!) {
|
|
50
|
+
request(id: $id) {
|
|
51
|
+
id host port method path query isTls createdAt
|
|
52
|
+
raw @include(if: $includeRequestRaw)
|
|
53
|
+
response { id statusCode roundtripTime length createdAt raw @include(if: $includeResponseRaw) }
|
|
54
|
+
}
|
|
55
|
+
}`
|
|
56
|
+
|
|
57
|
+
/** Empty replay session create (avoids the double history row the raw-create seeds). */
|
|
58
|
+
const CREATE_REPLAY_SESSION_DOC = `mutation CreateReplaySession($input: CreateReplaySessionInput!) {
|
|
59
|
+
createReplaySession(input: $input) { session { id } error { __typename } }
|
|
60
|
+
}`
|
|
61
|
+
|
|
62
|
+
/** Replay dispatch (strix `replay_send_raw` via the SDK's StartReplayTask). */
|
|
63
|
+
const START_REPLAY_TASK_DOC = `mutation StartReplayTask($sessionId: ID!, $input: StartReplayTaskInput!) {
|
|
64
|
+
startReplayTask(sessionId: $sessionId, input: $input) { error { __typename } task { id replayEntry { id } } }
|
|
65
|
+
}`
|
|
66
|
+
|
|
67
|
+
/** Replay entry poll — finished when request.response or error appears. */
|
|
68
|
+
const REPLAY_ENTRY_DOC = `query ReplayEntry($id: ID!, $includeReplayRaw: Boolean!, $includeRequestRaw: Boolean!, $includeResponseRaw: Boolean!) {
|
|
69
|
+
replayEntry(id: $id) {
|
|
70
|
+
id error
|
|
71
|
+
request { id method path response { id statusCode length roundtripTime raw @include(if: $includeResponseRaw) } }
|
|
72
|
+
}
|
|
73
|
+
}`
|
|
74
|
+
|
|
75
|
+
/** Scope management (strix caido_api.py scope_* via the SDK's ScopeFull set). */
|
|
76
|
+
const SCOPES_DOC = `query Scopes { scopes { id name allowlist denylist indexed } }`
|
|
77
|
+
|
|
78
|
+
const SCOPE_DOC = `query Scope($id: ID!) { scope(id: $id) { id name allowlist denylist indexed } }`
|
|
79
|
+
|
|
80
|
+
const CREATE_SCOPE_DOC = `mutation CreateScope($input: CreateScopeInput!) { createScope(input: $input) { error { __typename } scope { id name allowlist denylist indexed } } }`
|
|
81
|
+
|
|
82
|
+
const UPDATE_SCOPE_DOC = `mutation UpdateScope($id: ID!, $input: UpdateScopeInput!) { updateScope(id: $id, input: $input) { error { __typename } scope { id name allowlist denylist indexed } } }`
|
|
83
|
+
|
|
84
|
+
const DELETE_SCOPE_DOC = `mutation DeleteScope($id: ID!) { deleteScope(id: $id) { deletedId } }`
|
|
85
|
+
|
|
86
|
+
/** Sitemap queries (verbatim field sets from strix caido_api.py:555-592). */
|
|
87
|
+
const SITEMAP_ROOTS_DOC = `query GetSitemapRoots($scopeId: ID) {
|
|
88
|
+
sitemapRootEntries(scopeId: $scopeId) {
|
|
89
|
+
edges { node {
|
|
90
|
+
id kind label hasDescendants
|
|
91
|
+
metadata { ... on SitemapEntryMetadataDomain { isTls port } }
|
|
92
|
+
request { method path response { statusCode } }
|
|
93
|
+
} }
|
|
94
|
+
count { value }
|
|
95
|
+
}
|
|
96
|
+
}`
|
|
97
|
+
|
|
98
|
+
const SITEMAP_DESCENDANTS_DOC = `query GetSitemapDescendants($parentId: ID!, $depth: SitemapDescendantsDepth!) {
|
|
99
|
+
sitemapDescendantEntries(parentId: $parentId, depth: $depth) {
|
|
100
|
+
edges { node {
|
|
101
|
+
id kind label hasDescendants
|
|
102
|
+
request { method path response { statusCode } }
|
|
103
|
+
} }
|
|
104
|
+
count { value }
|
|
105
|
+
}
|
|
106
|
+
}`
|
|
107
|
+
|
|
108
|
+
const SITEMAP_ENTRY_DOC = `query GetSitemapEntry($id: ID!) {
|
|
109
|
+
sitemapEntry(id: $id) {
|
|
110
|
+
id kind label hasDescendants
|
|
111
|
+
metadata { ... on SitemapEntryMetadataDomain { isTls port } }
|
|
112
|
+
request { method path response { statusCode length roundtripTime } }
|
|
113
|
+
requests(first: 30, order: {by: CREATED_AT, ordering: DESC}) {
|
|
114
|
+
edges { node { method path response { statusCode length } } }
|
|
115
|
+
count { value }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}`
|
|
119
|
+
|
|
120
|
+
/** Fetch contract (global fetch shape) for the GraphQL endpoint. */
|
|
121
|
+
export interface CaidoFetchFn {
|
|
122
|
+
(url: string, init: { readonly method: 'POST'; readonly headers: Readonly<Record<string, string>>; readonly body: string; readonly signal: AbortSignal }): Promise<{ readonly status: number; readonly text: () => Promise<string> }>
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** One captured request entry projected for the model (strix shape). */
|
|
126
|
+
export interface RequestListEntry {
|
|
127
|
+
readonly cursor: string
|
|
128
|
+
readonly request: {
|
|
129
|
+
readonly id: string
|
|
130
|
+
readonly host: string
|
|
131
|
+
readonly port: number
|
|
132
|
+
readonly method: string
|
|
133
|
+
readonly path: string
|
|
134
|
+
readonly query: string | null
|
|
135
|
+
readonly tls: boolean
|
|
136
|
+
readonly createdAt: string
|
|
137
|
+
}
|
|
138
|
+
readonly response: { readonly id: string; readonly statusCode: number | null; readonly length: number | null; readonly createdAt: string | null } | null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** PageInfo projection (strix page_info). */
|
|
142
|
+
export interface CaidoPageInfo {
|
|
143
|
+
readonly hasNextPage: boolean
|
|
144
|
+
readonly hasPreviousPage: boolean
|
|
145
|
+
readonly startCursor: string | null
|
|
146
|
+
readonly endCursor: string | null
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The raw halves of one stored request. */
|
|
150
|
+
export interface StoredRequest {
|
|
151
|
+
readonly id: string
|
|
152
|
+
readonly host: string
|
|
153
|
+
readonly port: number
|
|
154
|
+
readonly method: string
|
|
155
|
+
readonly tls: boolean
|
|
156
|
+
readonly requestRaw: string | null
|
|
157
|
+
readonly responseRaw: string | null
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Replay outcome (strix `_format_replay_tool_result` inputs). */
|
|
161
|
+
export interface ReplayOutcome {
|
|
162
|
+
readonly sessionId: string
|
|
163
|
+
readonly status: string
|
|
164
|
+
readonly elapsedMs: number
|
|
165
|
+
readonly error?: string
|
|
166
|
+
readonly response: RawResponseParts | null
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Endpoint coordinates handed over by the sandbox session's bootstrap. */
|
|
170
|
+
export interface CaidoClientOptions {
|
|
171
|
+
readonly baseUrl: string
|
|
172
|
+
readonly token: string
|
|
173
|
+
readonly fetchFn?: CaidoFetchFn
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Minimal GraphQL POST with bearer auth and error normalization. */
|
|
177
|
+
async function graphql<T>(
|
|
178
|
+
options: CaidoClientOptions,
|
|
179
|
+
doc: string,
|
|
180
|
+
variables: Record<string, unknown>,
|
|
181
|
+
signal: AbortSignal,
|
|
182
|
+
): Promise<T> {
|
|
183
|
+
const fetchFn = options.fetchFn ?? fetch
|
|
184
|
+
const response = await fetchFn(`${options.baseUrl}/graphql`, {
|
|
185
|
+
method: 'POST',
|
|
186
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${options.token}` },
|
|
187
|
+
body: JSON.stringify({ query: doc, variables }),
|
|
188
|
+
signal,
|
|
189
|
+
})
|
|
190
|
+
const text = await response.text()
|
|
191
|
+
if (response.status !== 200) throw new Error(`caido graphql HTTP ${response.status}: ${text.slice(0, 200)}`)
|
|
192
|
+
let payload: unknown
|
|
193
|
+
try {
|
|
194
|
+
payload = JSON.parse(text)
|
|
195
|
+
} catch (error) {
|
|
196
|
+
throw new Error(`caido graphql unparseable response: ${String(error)}`)
|
|
197
|
+
}
|
|
198
|
+
const record = payload as { readonly data?: unknown; readonly errors?: unknown }
|
|
199
|
+
if (record.errors !== undefined && record.errors !== null) {
|
|
200
|
+
throw new Error(`caido graphql errors: ${JSON.stringify(record.errors).slice(0, 300)}`)
|
|
201
|
+
}
|
|
202
|
+
if (typeof record.data !== 'object' || record.data === null) {
|
|
203
|
+
throw new Error('caido graphql carried no data')
|
|
204
|
+
}
|
|
205
|
+
return record.data as T
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Project one raw requests-connection edge (strix list_requests mapping). */
|
|
209
|
+
function projectEdge(edge: unknown): RequestListEntry {
|
|
210
|
+
const node = (edge as { node: Record<string, unknown> }).node
|
|
211
|
+
const response = node.response as Record<string, unknown> | null
|
|
212
|
+
return {
|
|
213
|
+
cursor: String((edge as { cursor: unknown }).cursor),
|
|
214
|
+
request: {
|
|
215
|
+
id: String(node.id),
|
|
216
|
+
host: String(node.host),
|
|
217
|
+
port: Number(node.port),
|
|
218
|
+
method: String(node.method),
|
|
219
|
+
path: String(node.path),
|
|
220
|
+
query: node.query === null || node.query === undefined ? null : String(node.query),
|
|
221
|
+
tls: node.isTls === true,
|
|
222
|
+
createdAt: new Date(Number(node.createdAt)).toISOString(),
|
|
223
|
+
},
|
|
224
|
+
response: response === null || response === undefined ? null : {
|
|
225
|
+
id: String(response.id),
|
|
226
|
+
statusCode: response.statusCode === null || response.statusCode === undefined ? null : Number(response.statusCode),
|
|
227
|
+
length: response.length === null || response.length === undefined ? null : Number(response.length),
|
|
228
|
+
createdAt: response.createdAt === null || response.createdAt === undefined ? null : new Date(Number(response.createdAt)).toISOString(),
|
|
229
|
+
},
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* The Caido client: five operations behind the five proxy tools. All methods
|
|
235
|
+
* take an AbortSignal (tool exec.signal parity).
|
|
236
|
+
*/
|
|
237
|
+
export class CaidoClient {
|
|
238
|
+
private readonly options: CaidoClientOptions
|
|
239
|
+
|
|
240
|
+
constructor(options: CaidoClientOptions) {
|
|
241
|
+
this.options = options
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** List captured requests with HTTPQL filter/cursor/sort/scope (strix list_requests_with_client). */
|
|
245
|
+
async listRequests(options: {
|
|
246
|
+
readonly httpqlFilter?: string | undefined
|
|
247
|
+
readonly first?: number | undefined
|
|
248
|
+
readonly after?: string | undefined
|
|
249
|
+
readonly sortBy?: SortBy | undefined
|
|
250
|
+
readonly sortOrder?: 'asc' | 'desc' | undefined
|
|
251
|
+
readonly scopeId?: string | undefined
|
|
252
|
+
}, signal: AbortSignal): Promise<{ readonly entries: RequestListEntry[]; readonly pageInfo: CaidoPageInfo }> {
|
|
253
|
+
const data = await graphql<{ requests: { edges: unknown[]; pageInfo: Record<string, unknown> } }>(
|
|
254
|
+
this.options,
|
|
255
|
+
REQUESTS_DOC,
|
|
256
|
+
{
|
|
257
|
+
first: options.first ?? 50,
|
|
258
|
+
...(options.after !== undefined && options.after !== '' ? { after: options.after } : {}),
|
|
259
|
+
...(options.httpqlFilter !== undefined && options.httpqlFilter !== '' ? { filter: { code: options.httpqlFilter } } : {}),
|
|
260
|
+
order: { by: SORT_ENUMS[options.sortBy ?? 'timestamp'], ordering: (options.sortOrder ?? 'desc') === 'asc' ? 'ASC' : 'DESC' },
|
|
261
|
+
...(options.scopeId !== undefined && options.scopeId !== '' ? { scopeId: options.scopeId } : {}),
|
|
262
|
+
includeRequestRaw: false,
|
|
263
|
+
includeResponseRaw: false,
|
|
264
|
+
},
|
|
265
|
+
signal,
|
|
266
|
+
)
|
|
267
|
+
return {
|
|
268
|
+
entries: data.requests.edges.map(projectEdge),
|
|
269
|
+
pageInfo: {
|
|
270
|
+
hasNextPage: data.requests.pageInfo.hasNextPage === true,
|
|
271
|
+
hasPreviousPage: data.requests.pageInfo.hasPreviousPage === true,
|
|
272
|
+
startCursor: data.requests.pageInfo.startCursor === null || data.requests.pageInfo.startCursor === undefined ? null : String(data.requests.pageInfo.startCursor),
|
|
273
|
+
endCursor: data.requests.pageInfo.endCursor === null || data.requests.pageInfo.endCursor === undefined ? null : String(data.requests.pageInfo.endCursor),
|
|
274
|
+
},
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Fetch one request with both raw halves (strix get_request_with_client parity: always request both). */
|
|
279
|
+
async getRequest(requestId: string, signal: AbortSignal): Promise<StoredRequest | null> {
|
|
280
|
+
const data = await graphql<{ request: Record<string, unknown> | null }>(
|
|
281
|
+
this.options,
|
|
282
|
+
REQUEST_DOC,
|
|
283
|
+
{ id: requestId, includeRequestRaw: true, includeResponseRaw: true },
|
|
284
|
+
signal,
|
|
285
|
+
)
|
|
286
|
+
const request = data.request
|
|
287
|
+
if (request === null || request === undefined) return null
|
|
288
|
+
return {
|
|
289
|
+
id: String(request.id),
|
|
290
|
+
host: String(request.host),
|
|
291
|
+
port: Number(request.port),
|
|
292
|
+
method: String(request.method),
|
|
293
|
+
tls: request.isTls === true,
|
|
294
|
+
requestRaw: request.raw === null || request.raw === undefined ? null : Buffer.from(String(request.raw), 'base64').toString('utf8'),
|
|
295
|
+
responseRaw: (request.response as Record<string, unknown> | null | undefined)?.raw === null
|
|
296
|
+
|| (request.response as Record<string, unknown> | null | undefined)?.raw === undefined
|
|
297
|
+
? null
|
|
298
|
+
: Buffer.from(String((request.response as Record<string, unknown>).raw), 'base64').toString('utf8'),
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Replay one stored request with optional field patches (strix
|
|
304
|
+
* `repeat_request` + `replay_send_raw`): fetch raw → parse → patch →
|
|
305
|
+
* rebuild → create empty session → startReplayTask → poll the entry.
|
|
306
|
+
* @param requestId - the stored request id.
|
|
307
|
+
* @param modifications - patch dict (url/params/headers/body/cookies).
|
|
308
|
+
* @param budget - total dispatch deadline (strix 30s) + poll interval.
|
|
309
|
+
*/
|
|
310
|
+
async replayRequest(
|
|
311
|
+
requestId: string,
|
|
312
|
+
modifications: Readonly<Record<string, unknown>> | undefined,
|
|
313
|
+
signal: AbortSignal,
|
|
314
|
+
budget: { readonly dispatchTimeoutMs: number; readonly pollIntervalMs: number },
|
|
315
|
+
): Promise<ReplayOutcome | null> {
|
|
316
|
+
const stored = await this.getRequest(requestId, signal)
|
|
317
|
+
if (stored === null || stored.requestRaw === null) return null
|
|
318
|
+
const components = parseRawRequest(stored.requestRaw)
|
|
319
|
+
const fullUrl = fullUrlFromComponents({ host: stored.host, tls: stored.tls }, components, modifications ?? {})
|
|
320
|
+
const modified = applyModifications(components, modifications ?? {}, fullUrl)
|
|
321
|
+
const built = buildRawRequest(modified)
|
|
322
|
+
|
|
323
|
+
const session = await graphql<{ createReplaySession: { session: { id: string } | null; error: unknown } }>(
|
|
324
|
+
this.options,
|
|
325
|
+
CREATE_REPLAY_SESSION_DOC,
|
|
326
|
+
{ input: {} },
|
|
327
|
+
signal,
|
|
328
|
+
)
|
|
329
|
+
const createdSession = session.createReplaySession.session
|
|
330
|
+
if (createdSession === null || createdSession === undefined) throw new Error('createReplaySession returned no session')
|
|
331
|
+
|
|
332
|
+
const started = Date.now()
|
|
333
|
+
const start = await graphql<{ startReplayTask: { error: unknown; task: { id: string; replayEntry: { id: string } | null } | null } }>(
|
|
334
|
+
this.options,
|
|
335
|
+
START_REPLAY_TASK_DOC,
|
|
336
|
+
{
|
|
337
|
+
sessionId: createdSession.id,
|
|
338
|
+
input: {
|
|
339
|
+
connection: { host: built.connection.host, port: built.connection.port, isTLS: built.connection.tls, SNI: null },
|
|
340
|
+
raw: Buffer.from(built.raw).toString('base64'),
|
|
341
|
+
settings: { connectionClose: false, updateContentLength: true, placeholders: [] },
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
signal,
|
|
345
|
+
)
|
|
346
|
+
const task = start.startReplayTask.task
|
|
347
|
+
if (start.startReplayTask.error !== null && start.startReplayTask.error !== undefined) {
|
|
348
|
+
throw new Error(`startReplayTask failed: ${JSON.stringify(start.startReplayTask.error).slice(0, 200)}`)
|
|
349
|
+
}
|
|
350
|
+
if (task === null || task === undefined || task.replayEntry === null || task.replayEntry === undefined) {
|
|
351
|
+
throw new Error('startReplayTask returned no task/entry')
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Bounded poll (SDK waits on a subscription; polling keeps us HTTP-only).
|
|
355
|
+
for (;;) {
|
|
356
|
+
const entry = await graphql<{ replayEntry: Record<string, unknown> | null }>(
|
|
357
|
+
this.options,
|
|
358
|
+
REPLAY_ENTRY_DOC,
|
|
359
|
+
{ id: task.replayEntry.id, includeReplayRaw: false, includeRequestRaw: false, includeResponseRaw: true },
|
|
360
|
+
signal,
|
|
361
|
+
)
|
|
362
|
+
const node = entry.replayEntry
|
|
363
|
+
if (node !== null && node !== undefined) {
|
|
364
|
+
const errorText = node.error === null || node.error === undefined ? undefined : String(node.error)
|
|
365
|
+
const request = node.request as { response?: Record<string, unknown> | null } | null | undefined
|
|
366
|
+
const responseNode = request?.response ?? null
|
|
367
|
+
if (responseNode !== null && responseNode !== undefined) {
|
|
368
|
+
const rawBase64 = responseNode.raw
|
|
369
|
+
const rawBytes = rawBase64 === null || rawBase64 === undefined ? null : Buffer.from(String(rawBase64), 'base64')
|
|
370
|
+
return {
|
|
371
|
+
sessionId: createdSession.id,
|
|
372
|
+
status: 'DONE',
|
|
373
|
+
elapsedMs: Date.now() - started,
|
|
374
|
+
...(errorText !== undefined ? { error: errorText } : {}),
|
|
375
|
+
response: parseRawResponse(rawBytes),
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (errorText !== undefined) {
|
|
379
|
+
return { sessionId: createdSession.id, status: 'ERROR', elapsedMs: Date.now() - started, error: errorText, response: null }
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (Date.now() - started > budget.dispatchTimeoutMs) {
|
|
383
|
+
return {
|
|
384
|
+
sessionId: createdSession.id,
|
|
385
|
+
status: 'ERROR',
|
|
386
|
+
elapsedMs: Date.now() - started,
|
|
387
|
+
error: `Caido replay dispatch did not complete within ${String(Math.round(budget.dispatchTimeoutMs / 1000))}s — the target may be unroutable from the sandbox, or Caido's outbound HTTP client is stalled; check the target host/port and retry`,
|
|
388
|
+
response: null,
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
await new Promise(resolve => setTimeout(resolve, budget.pollIntervalMs))
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Sitemap roots or descendants (strix list_sitemap_with_client). */
|
|
396
|
+
async listSitemap(options: {
|
|
397
|
+
readonly scopeId?: string | undefined
|
|
398
|
+
readonly parentId?: string | undefined
|
|
399
|
+
readonly depth?: 'DIRECT' | 'ALL' | undefined
|
|
400
|
+
}, signal: AbortSignal): Promise<unknown> {
|
|
401
|
+
if (options.parentId !== undefined && options.parentId !== '') {
|
|
402
|
+
return graphql(this.options, SITEMAP_DESCENDANTS_DOC, { parentId: options.parentId, depth: options.depth ?? 'DIRECT' }, signal)
|
|
403
|
+
}
|
|
404
|
+
return graphql(this.options, SITEMAP_ROOTS_DOC, options.scopeId !== undefined && options.scopeId !== '' ? { scopeId: options.scopeId } : {}, signal)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** One sitemap entry with its 30 most recent requests (strix view_sitemap_entry_with_client). */
|
|
408
|
+
async viewSitemapEntry(entryId: string, signal: AbortSignal): Promise<unknown> {
|
|
409
|
+
return graphql(this.options, SITEMAP_ENTRY_DOC, { id: entryId }, signal)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** All Caido scopes (strix scope_list). */
|
|
413
|
+
async scopeList(signal: AbortSignal): Promise<unknown> {
|
|
414
|
+
return graphql(this.options, SCOPES_DOC, {}, signal)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** One scope by id (strix scope_get). */
|
|
418
|
+
async scopeGet(scopeId: string, signal: AbortSignal): Promise<unknown> {
|
|
419
|
+
return graphql(this.options, SCOPE_DOC, { id: scopeId }, signal)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** Create a scope; empty lists allow-all/deny-none (strix scope_create). */
|
|
423
|
+
async scopeCreate(name: string, allowlist: string[], denylist: string[], signal: AbortSignal): Promise<unknown> {
|
|
424
|
+
return graphql(this.options, CREATE_SCOPE_DOC, { input: { name, allowlist, denylist } }, signal)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** Update a scope; allow/deny lists FULLY REPLACE the previous values (strix parity). */
|
|
428
|
+
async scopeUpdate(scopeId: string, name: string, allowlist: string[], denylist: string[], signal: AbortSignal): Promise<unknown> {
|
|
429
|
+
return graphql(this.options, UPDATE_SCOPE_DOC, { id: scopeId, input: { name, allowlist, denylist } }, signal)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Delete a scope (strix scope_delete). */
|
|
433
|
+
async scopeDelete(scopeId: string, signal: AbortSignal): Promise<unknown> {
|
|
434
|
+
return graphql(this.options, DELETE_SCOPE_DOC, { id: scopeId }, signal)
|
|
435
|
+
}
|
|
436
|
+
}
|