vibesane 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +116 -2
- package/bin/vibesane.js +3 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +79 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +121 -0
- package/dist/reporter/ai-prompt-gen.d.ts +18 -0
- package/dist/reporter/ai-prompt-gen.js +101 -0
- package/dist/reporter/terminal-ui.d.ts +14 -0
- package/dist/reporter/terminal-ui.js +103 -0
- package/dist/scanner/port-detector.d.ts +48 -0
- package/dist/scanner/port-detector.js +335 -0
- package/dist/scanner/route-crawler.d.ts +39 -0
- package/dist/scanner/route-crawler.js +670 -0
- package/dist/scanner/security-scan.d.ts +29 -0
- package/dist/scanner/security-scan.js +750 -0
- package/dist/text/ascii.d.ts +1 -0
- package/dist/text/ascii.js +51 -0
- package/package.json +54 -4
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export declare const DEFAULT_PORTS: readonly [3000, 3001, 5173, 5174, 8080, 8000, 4321];
|
|
2
|
+
export declare const DEFAULT_TCP_TIMEOUT_MS = 300;
|
|
3
|
+
export declare const DEFAULT_HTTP_TIMEOUT_MS = 2000;
|
|
4
|
+
export declare const DEFAULT_BODY_READ_TIMEOUT_MS = 500;
|
|
5
|
+
export declare const MAX_BODY_BYTES: number;
|
|
6
|
+
export type LoopbackHost = '127.0.0.1' | '::1';
|
|
7
|
+
export type DetectionFailureReason = 'no_server' | 'non_http' | 'http_timeout';
|
|
8
|
+
export interface PortDetectorOptions {
|
|
9
|
+
port?: number;
|
|
10
|
+
ports?: readonly number[];
|
|
11
|
+
tcpTimeoutMs?: number;
|
|
12
|
+
httpTimeoutMs?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface TcpProbeResult {
|
|
15
|
+
port: number;
|
|
16
|
+
open: boolean;
|
|
17
|
+
host?: LoopbackHost;
|
|
18
|
+
elapsedMs: number;
|
|
19
|
+
}
|
|
20
|
+
export interface RootHttpSnapshot {
|
|
21
|
+
path: string;
|
|
22
|
+
statusCode: number;
|
|
23
|
+
contentType: string;
|
|
24
|
+
location?: string;
|
|
25
|
+
body: string;
|
|
26
|
+
headersElapsedMs: number;
|
|
27
|
+
truncated: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface PortDetectionResult {
|
|
30
|
+
ok: boolean;
|
|
31
|
+
port: number | null;
|
|
32
|
+
host?: LoopbackHost;
|
|
33
|
+
url?: string;
|
|
34
|
+
statusCode?: number;
|
|
35
|
+
elapsedMs: number;
|
|
36
|
+
alsoActivePorts: number[];
|
|
37
|
+
checkedPorts: number[];
|
|
38
|
+
reason?: DetectionFailureReason;
|
|
39
|
+
rootResponse?: RootHttpSnapshot;
|
|
40
|
+
}
|
|
41
|
+
export declare function assertValidPort(port: number): void;
|
|
42
|
+
export declare function assertValidTimeout(ms: number, name?: string): void;
|
|
43
|
+
export declare function readBoundedTextBody(res: Response, maxBytes?: number, bodyTimeoutMs?: number): Promise<{
|
|
44
|
+
body: string;
|
|
45
|
+
truncated: boolean;
|
|
46
|
+
}>;
|
|
47
|
+
export declare function probeTcpPort(port: number, tcpTimeoutMs?: number, signal?: AbortSignal): Promise<TcpProbeResult>;
|
|
48
|
+
export declare function detectServer(options?: PortDetectorOptions): Promise<PortDetectionResult>;
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
import { performance } from 'node:perf_hooks';
|
|
3
|
+
export const DEFAULT_PORTS = [3000, 3001, 5173, 5174, 8080, 8000, 4321];
|
|
4
|
+
export const DEFAULT_TCP_TIMEOUT_MS = 300;
|
|
5
|
+
export const DEFAULT_HTTP_TIMEOUT_MS = 2000;
|
|
6
|
+
export const DEFAULT_BODY_READ_TIMEOUT_MS = 500;
|
|
7
|
+
export const MAX_BODY_BYTES = 1024 * 1024;
|
|
8
|
+
export function assertValidPort(port) {
|
|
9
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
10
|
+
throw new RangeError(`Invalid port "${port}": expected integer between 1 and 65535`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function assertValidTimeout(ms, name = 'timeout') {
|
|
14
|
+
if (typeof ms !== 'number' || !Number.isFinite(ms) || ms <= 0) {
|
|
15
|
+
throw new RangeError(`Invalid ${name} "${ms}": expected positive finite number`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function isTextLikeContentType(contentType) {
|
|
19
|
+
const lower = contentType.toLowerCase();
|
|
20
|
+
if (lower === '') {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
return (lower.includes('text/') ||
|
|
24
|
+
lower.includes('html') ||
|
|
25
|
+
lower.includes('json') ||
|
|
26
|
+
lower.includes('javascript') ||
|
|
27
|
+
lower.includes('ecmascript') ||
|
|
28
|
+
lower.includes('xml'));
|
|
29
|
+
}
|
|
30
|
+
export async function readBoundedTextBody(res, maxBytes = MAX_BODY_BYTES, bodyTimeoutMs = DEFAULT_BODY_READ_TIMEOUT_MS) {
|
|
31
|
+
const contentType = (res.headers.get('content-type') ?? '').toLowerCase();
|
|
32
|
+
if (!isTextLikeContentType(contentType)) {
|
|
33
|
+
await res.body?.cancel().catch(() => { });
|
|
34
|
+
return { body: '', truncated: false };
|
|
35
|
+
}
|
|
36
|
+
if (!res.body) {
|
|
37
|
+
return { body: '', truncated: false };
|
|
38
|
+
}
|
|
39
|
+
const reader = res.body.getReader();
|
|
40
|
+
const decoder = new TextDecoder('utf-8');
|
|
41
|
+
const parts = [];
|
|
42
|
+
let totalBytes = 0;
|
|
43
|
+
let truncated = false;
|
|
44
|
+
let timedOut = false;
|
|
45
|
+
let timer = null;
|
|
46
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
47
|
+
timer = setTimeout(() => {
|
|
48
|
+
timedOut = true;
|
|
49
|
+
truncated = true;
|
|
50
|
+
void reader.cancel().catch(() => { });
|
|
51
|
+
resolve({ kind: 'timeout' });
|
|
52
|
+
}, bodyTimeoutMs);
|
|
53
|
+
});
|
|
54
|
+
try {
|
|
55
|
+
while (!timedOut) {
|
|
56
|
+
const readPromise = reader.read().then((result) => ({ kind: 'chunk', result }), () => ({ kind: 'error' }));
|
|
57
|
+
const outcome = await Promise.race([readPromise, timeoutPromise]);
|
|
58
|
+
if (outcome.kind === 'timeout') {
|
|
59
|
+
truncated = true;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
if (outcome.kind === 'error') {
|
|
63
|
+
truncated = true;
|
|
64
|
+
await reader.cancel().catch(() => { });
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
const { done, value: chunk } = outcome.result;
|
|
68
|
+
if (done) {
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
if (!chunk || chunk.byteLength === 0) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const remaining = maxBytes - totalBytes;
|
|
75
|
+
if (chunk.byteLength >= remaining) {
|
|
76
|
+
const slice = chunk.subarray(0, remaining);
|
|
77
|
+
if (slice.byteLength > 0) {
|
|
78
|
+
parts.push(decoder.decode(slice, { stream: true }));
|
|
79
|
+
totalBytes += slice.byteLength;
|
|
80
|
+
}
|
|
81
|
+
truncated = true;
|
|
82
|
+
await reader.cancel().catch(() => { });
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
parts.push(decoder.decode(chunk, { stream: true }));
|
|
86
|
+
totalBytes += chunk.byteLength;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
truncated = true;
|
|
91
|
+
await reader.cancel().catch(() => { });
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
if (timer !== null) {
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
parts.push(decoder.decode());
|
|
99
|
+
return { body: parts.join(''), truncated };
|
|
100
|
+
}
|
|
101
|
+
export function probeTcpPort(port, tcpTimeoutMs = DEFAULT_TCP_TIMEOUT_MS, signal) {
|
|
102
|
+
assertValidPort(port);
|
|
103
|
+
assertValidTimeout(tcpTimeoutMs, 'tcpTimeoutMs');
|
|
104
|
+
const startedAt = performance.now();
|
|
105
|
+
return new Promise((resolve) => {
|
|
106
|
+
if (signal?.aborted) {
|
|
107
|
+
resolve({
|
|
108
|
+
port,
|
|
109
|
+
open: false,
|
|
110
|
+
elapsedMs: Math.max(0, Math.round(performance.now() - startedAt)),
|
|
111
|
+
});
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
let settled = false;
|
|
115
|
+
let failedCount = 0;
|
|
116
|
+
const hosts = ['127.0.0.1', '::1'];
|
|
117
|
+
const sockets = [];
|
|
118
|
+
const onAbort = () => {
|
|
119
|
+
finish(false);
|
|
120
|
+
};
|
|
121
|
+
const timer = setTimeout(() => {
|
|
122
|
+
finish(false);
|
|
123
|
+
}, tcpTimeoutMs);
|
|
124
|
+
const cleanup = () => {
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
signal?.removeEventListener('abort', onAbort);
|
|
127
|
+
for (const socket of sockets) {
|
|
128
|
+
socket.removeAllListeners();
|
|
129
|
+
socket.destroy();
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
const finish = (open, winningHost) => {
|
|
133
|
+
if (settled) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
settled = true;
|
|
137
|
+
cleanup();
|
|
138
|
+
const elapsedMs = Math.max(0, Math.round(performance.now() - startedAt));
|
|
139
|
+
if (open && winningHost) {
|
|
140
|
+
resolve({ port, open: true, host: winningHost, elapsedMs });
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
resolve({ port, open: false, elapsedMs });
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
147
|
+
for (const host of hosts) {
|
|
148
|
+
const socket = net.createConnection({ host, port });
|
|
149
|
+
sockets.push(socket);
|
|
150
|
+
socket.once('connect', () => {
|
|
151
|
+
finish(true, host);
|
|
152
|
+
});
|
|
153
|
+
socket.once('error', () => {
|
|
154
|
+
failedCount += 1;
|
|
155
|
+
if (failedCount >= hosts.length) {
|
|
156
|
+
finish(false);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
export async function detectServer(options = {}) {
|
|
163
|
+
const startedAt = performance.now();
|
|
164
|
+
const tcpTimeoutMs = options.tcpTimeoutMs ?? DEFAULT_TCP_TIMEOUT_MS;
|
|
165
|
+
const httpTimeoutMs = options.httpTimeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS;
|
|
166
|
+
assertValidTimeout(tcpTimeoutMs, 'tcpTimeoutMs');
|
|
167
|
+
assertValidTimeout(httpTimeoutMs, 'httpTimeoutMs');
|
|
168
|
+
let checkedPorts;
|
|
169
|
+
if (options.port !== undefined) {
|
|
170
|
+
assertValidPort(options.port);
|
|
171
|
+
checkedPorts = [options.port];
|
|
172
|
+
}
|
|
173
|
+
else if (options.ports !== undefined) {
|
|
174
|
+
if (options.ports.length === 0) {
|
|
175
|
+
throw new RangeError('ports list cannot be empty');
|
|
176
|
+
}
|
|
177
|
+
for (const p of options.ports) {
|
|
178
|
+
assertValidPort(p);
|
|
179
|
+
}
|
|
180
|
+
checkedPorts = [...new Set(options.ports)];
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
checkedPorts = [...DEFAULT_PORTS];
|
|
184
|
+
}
|
|
185
|
+
const controllers = checkedPorts.map(() => new AbortController());
|
|
186
|
+
const results = new Array(checkedPorts.length).fill(null);
|
|
187
|
+
let winnerIndex = -1;
|
|
188
|
+
let resolveStage1;
|
|
189
|
+
const stage1Done = new Promise((resolve) => {
|
|
190
|
+
resolveStage1 = resolve;
|
|
191
|
+
});
|
|
192
|
+
let graceStarted = false;
|
|
193
|
+
let graceTimer = null;
|
|
194
|
+
let settledStage1 = false;
|
|
195
|
+
const finishStage1 = () => {
|
|
196
|
+
if (settledStage1) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
settledStage1 = true;
|
|
200
|
+
if (graceTimer !== null) {
|
|
201
|
+
clearTimeout(graceTimer);
|
|
202
|
+
graceTimer = null;
|
|
203
|
+
}
|
|
204
|
+
if (winnerIndex !== -1) {
|
|
205
|
+
for (let k = winnerIndex + 1; k < checkedPorts.length; k++) {
|
|
206
|
+
if (results[k] === null) {
|
|
207
|
+
controllers[k].abort();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
resolveStage1();
|
|
212
|
+
};
|
|
213
|
+
const evaluateProgress = () => {
|
|
214
|
+
if (settledStage1) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (winnerIndex === -1) {
|
|
218
|
+
let candidate = -1;
|
|
219
|
+
for (let i = 0; i < checkedPorts.length; i++) {
|
|
220
|
+
const res = results[i];
|
|
221
|
+
if (res === null) {
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
if (res.open) {
|
|
225
|
+
candidate = i;
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (candidate !== -1) {
|
|
230
|
+
winnerIndex = candidate;
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
const allFinished = results.every((r) => r !== null);
|
|
234
|
+
if (allFinished) {
|
|
235
|
+
finishStage1();
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const anyLowerPriorityPending = results
|
|
241
|
+
.slice(winnerIndex + 1)
|
|
242
|
+
.some((r) => r === null);
|
|
243
|
+
if (!anyLowerPriorityPending) {
|
|
244
|
+
finishStage1();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (!graceStarted) {
|
|
248
|
+
graceStarted = true;
|
|
249
|
+
const graceMs = Math.min(15, Math.floor(tcpTimeoutMs / 5));
|
|
250
|
+
if (graceMs <= 0) {
|
|
251
|
+
finishStage1();
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
graceTimer = setTimeout(() => {
|
|
255
|
+
finishStage1();
|
|
256
|
+
}, graceMs);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
const probePromises = checkedPorts.map((port, index) => probeTcpPort(port, tcpTimeoutMs, controllers[index].signal).then((res) => {
|
|
261
|
+
results[index] = res;
|
|
262
|
+
evaluateProgress();
|
|
263
|
+
return res;
|
|
264
|
+
}));
|
|
265
|
+
await stage1Done;
|
|
266
|
+
await Promise.all(probePromises);
|
|
267
|
+
if (winnerIndex === -1) {
|
|
268
|
+
return {
|
|
269
|
+
ok: false,
|
|
270
|
+
port: null,
|
|
271
|
+
elapsedMs: Math.max(0, Math.round(performance.now() - startedAt)),
|
|
272
|
+
alsoActivePorts: [],
|
|
273
|
+
checkedPorts,
|
|
274
|
+
reason: 'no_server',
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
const winner = results[winnerIndex];
|
|
278
|
+
const winnerPort = winner.port;
|
|
279
|
+
const winnerHost = winner.host ?? '127.0.0.1';
|
|
280
|
+
const alsoActivePorts = [];
|
|
281
|
+
for (let k = winnerIndex + 1; k < checkedPorts.length; k++) {
|
|
282
|
+
const res = results[k];
|
|
283
|
+
if (res?.open) {
|
|
284
|
+
alsoActivePorts.push(checkedPorts[k]);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const fetchHost = winnerHost === '::1' ? '[::1]' : '127.0.0.1';
|
|
288
|
+
const targetUrl = `http://${fetchHost}:${winnerPort}/`;
|
|
289
|
+
const displayUrl = `http://localhost:${winnerPort}`;
|
|
290
|
+
try {
|
|
291
|
+
const fetchStart = performance.now();
|
|
292
|
+
const res = await fetch(targetUrl, {
|
|
293
|
+
method: 'GET',
|
|
294
|
+
redirect: 'manual',
|
|
295
|
+
signal: AbortSignal.timeout(httpTimeoutMs),
|
|
296
|
+
});
|
|
297
|
+
const headersElapsedMs = Math.max(0, Math.round(performance.now() - fetchStart));
|
|
298
|
+
const contentType = res.headers.get('content-type') ?? '';
|
|
299
|
+
const location = res.headers.get('location') ?? undefined;
|
|
300
|
+
const { body, truncated } = await readBoundedTextBody(res, MAX_BODY_BYTES, DEFAULT_BODY_READ_TIMEOUT_MS);
|
|
301
|
+
return {
|
|
302
|
+
ok: true,
|
|
303
|
+
port: winnerPort,
|
|
304
|
+
host: winnerHost,
|
|
305
|
+
url: displayUrl,
|
|
306
|
+
statusCode: res.status,
|
|
307
|
+
elapsedMs: Math.max(0, Math.round(performance.now() - startedAt)),
|
|
308
|
+
alsoActivePorts,
|
|
309
|
+
checkedPorts,
|
|
310
|
+
rootResponse: {
|
|
311
|
+
path: '/',
|
|
312
|
+
statusCode: res.status,
|
|
313
|
+
contentType,
|
|
314
|
+
location,
|
|
315
|
+
body,
|
|
316
|
+
headersElapsedMs,
|
|
317
|
+
truncated,
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
catch (err) {
|
|
322
|
+
const isTimeout = err instanceof Error &&
|
|
323
|
+
(err.name === 'TimeoutError' || err.name === 'AbortError');
|
|
324
|
+
return {
|
|
325
|
+
ok: false,
|
|
326
|
+
port: winnerPort,
|
|
327
|
+
host: winnerHost,
|
|
328
|
+
url: displayUrl,
|
|
329
|
+
elapsedMs: Math.max(0, Math.round(performance.now() - startedAt)),
|
|
330
|
+
alsoActivePorts,
|
|
331
|
+
checkedPorts,
|
|
332
|
+
reason: isTimeout ? 'http_timeout' : 'non_http',
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type LoopbackHost, type RootHttpSnapshot } from './port-detector.js';
|
|
2
|
+
import { type SanityIssue } from './security-scan.js';
|
|
3
|
+
export declare const DEFAULT_SCAN_BUDGET_MS = 2500;
|
|
4
|
+
export declare const DEFAULT_ROUTE_TIMEOUT_MS = 2000;
|
|
5
|
+
export declare const DEFAULT_SLOW_ROUTE_MS = 1500;
|
|
6
|
+
export declare const DEFAULT_MAX_ROUTES = 10;
|
|
7
|
+
export declare const DEFAULT_MAX_SCRIPTS = 5;
|
|
8
|
+
export declare const DEFAULT_MAX_CONCURRENCY = 4;
|
|
9
|
+
export declare const MAX_REDIRECTS = 3;
|
|
10
|
+
export interface RouteScanItem {
|
|
11
|
+
path: string;
|
|
12
|
+
statusCode?: number;
|
|
13
|
+
contentType?: string;
|
|
14
|
+
headersElapsedMs?: number;
|
|
15
|
+
truncated?: boolean;
|
|
16
|
+
skipped?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface SanityScanOptions {
|
|
19
|
+
scanBudgetMs?: number;
|
|
20
|
+
routeTimeoutMs?: number;
|
|
21
|
+
slowRouteMs?: number;
|
|
22
|
+
maxRoutes?: number;
|
|
23
|
+
maxScripts?: number;
|
|
24
|
+
maxConcurrency?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface SanityScanSummary {
|
|
27
|
+
routes: RouteScanItem[];
|
|
28
|
+
scannedScripts: string[];
|
|
29
|
+
skippedRoutes: string[];
|
|
30
|
+
issues: SanityIssue[];
|
|
31
|
+
}
|
|
32
|
+
export declare function isExcludedFrameworkScript(pathOrUrl: string): boolean;
|
|
33
|
+
export declare function getScriptPriority(pathOrUrl: string): 1 | 2;
|
|
34
|
+
export declare function extractInternalLinks(html: string, baseOrigin: string, maxSubRoutes: number): string[];
|
|
35
|
+
export declare function extractCandidateScripts(html: string, baseOrigin: string, maxScripts: number): string[];
|
|
36
|
+
export declare function selectScriptsToFetch(input: string | readonly string[], baseOriginOrMaxScripts?: string | number, maxScripts?: number): string[];
|
|
37
|
+
export declare function extractModuleImports(jsContent: string, scriptPath: string, baseOrigin: string): string[];
|
|
38
|
+
export declare function fetchSameOriginHop(baseUrl: string, initialPath: string, signal: AbortSignal, routeTimeoutMs: number, followRedirects?: boolean): Promise<RootHttpSnapshot>;
|
|
39
|
+
export declare function runSanityScan(host: LoopbackHost, port: number, rootResponse: RootHttpSnapshot, options?: SanityScanOptions): Promise<SanityScanSummary>;
|