@sackville-mcp/api 0.0.1-alpha.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 +201 -0
- package/dist/index.d.mts +898 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +3221 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +44 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,898 @@
|
|
|
1
|
+
import { AssertionOp } from "@sackville-mcp/assert";
|
|
2
|
+
import { DnsLookup, Redactor } from "@sackville-mcp/safety";
|
|
3
|
+
import { FormData } from "undici";
|
|
4
|
+
|
|
5
|
+
//#region src/artifacts.d.ts
|
|
6
|
+
interface Artifact {
|
|
7
|
+
contentType: string;
|
|
8
|
+
body: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* In-memory store for response bodies, addressed by a `sackville://run/<id>/body`
|
|
12
|
+
* handle. Agents/CLIs fetch bodies by handle so large payloads are never inlined
|
|
13
|
+
* into tool results. (A persistent backend can replace this later.)
|
|
14
|
+
*/
|
|
15
|
+
declare class ArtifactStore {
|
|
16
|
+
private artifacts;
|
|
17
|
+
put(runId: string, body: string, contentType: string): string;
|
|
18
|
+
get(handle: string): Artifact | undefined;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/model.d.ts
|
|
22
|
+
type AssertionSource = 'status' | 'statusText' | 'header' | 'body' | 'jsonpath' | 'responseTime' | 'schema';
|
|
23
|
+
/** A declarative assertion (from a `*.sackville.yml` sidecar). */
|
|
24
|
+
interface AssertionSpec {
|
|
25
|
+
source: AssertionSource;
|
|
26
|
+
op: AssertionOp;
|
|
27
|
+
value?: unknown;
|
|
28
|
+
/** JSONPath expression for source 'jsonpath'. */
|
|
29
|
+
path?: string;
|
|
30
|
+
/** Header name for source 'header'. */
|
|
31
|
+
name?: string;
|
|
32
|
+
}
|
|
33
|
+
/** Capture a value from a response into the runtime variable scope. */
|
|
34
|
+
interface CaptureSpec {
|
|
35
|
+
var: string;
|
|
36
|
+
source: 'status' | 'header' | 'body' | 'jsonpath';
|
|
37
|
+
path?: string;
|
|
38
|
+
name?: string;
|
|
39
|
+
}
|
|
40
|
+
/** A multipart-form part: a `text` field or a `file` upload (by path). */
|
|
41
|
+
interface MultipartPart {
|
|
42
|
+
name: string;
|
|
43
|
+
kind: 'text' | 'file';
|
|
44
|
+
/** Field value (text parts). */
|
|
45
|
+
value?: string;
|
|
46
|
+
/** Source path(s) on disk (file parts; Bruno allows multiple). */
|
|
47
|
+
filePaths?: string[];
|
|
48
|
+
/** Explicit per-part content type (file parts), when set. */
|
|
49
|
+
contentType?: string;
|
|
50
|
+
}
|
|
51
|
+
/** A raw file body — the file's bytes sent as the request body. */
|
|
52
|
+
interface FileBody {
|
|
53
|
+
filePath: string;
|
|
54
|
+
contentType?: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A request body. Raw types (`json`/`text`/`xml`/`sparql`) carry `content`;
|
|
58
|
+
* `form-urlencoded` carries `params`; `graphql` carries a query + variables;
|
|
59
|
+
* `multipart-form` carries `parts`; `file` carries a single `file`.
|
|
60
|
+
*/
|
|
61
|
+
interface RequestBody {
|
|
62
|
+
type: string;
|
|
63
|
+
content?: string;
|
|
64
|
+
params?: {
|
|
65
|
+
name: string;
|
|
66
|
+
value: string;
|
|
67
|
+
}[];
|
|
68
|
+
graphql?: {
|
|
69
|
+
query: string;
|
|
70
|
+
variables?: string;
|
|
71
|
+
};
|
|
72
|
+
parts?: MultipartPart[];
|
|
73
|
+
file?: FileBody;
|
|
74
|
+
}
|
|
75
|
+
interface ApiRequest {
|
|
76
|
+
name: string;
|
|
77
|
+
method: string;
|
|
78
|
+
url: string;
|
|
79
|
+
headers: {
|
|
80
|
+
name: string;
|
|
81
|
+
value: string;
|
|
82
|
+
}[];
|
|
83
|
+
body?: RequestBody;
|
|
84
|
+
}
|
|
85
|
+
interface RequestEntry {
|
|
86
|
+
request: ApiRequest;
|
|
87
|
+
assertions: AssertionSpec[];
|
|
88
|
+
captures: CaptureSpec[];
|
|
89
|
+
/** Optional pre-request / post-response sandboxed scripts (from the sidecar). */
|
|
90
|
+
preScript?: string;
|
|
91
|
+
postScript?: string;
|
|
92
|
+
}
|
|
93
|
+
/** Result of a `test(name, fn)` in a sandboxed script. */
|
|
94
|
+
interface ScriptTest {
|
|
95
|
+
name: string;
|
|
96
|
+
pass: boolean;
|
|
97
|
+
error?: string;
|
|
98
|
+
}
|
|
99
|
+
interface Collection {
|
|
100
|
+
dir: string;
|
|
101
|
+
requests: Map<string, RequestEntry>;
|
|
102
|
+
/** Environment name → its (non-secret) variables. */
|
|
103
|
+
environments: Map<string, Record<string, string>>;
|
|
104
|
+
}
|
|
105
|
+
interface AssertionResult {
|
|
106
|
+
source: AssertionSource;
|
|
107
|
+
op: AssertionOp;
|
|
108
|
+
path?: string;
|
|
109
|
+
name?: string;
|
|
110
|
+
expected?: unknown;
|
|
111
|
+
actual: unknown;
|
|
112
|
+
pass: boolean;
|
|
113
|
+
}
|
|
114
|
+
/** Contract-validation finding kinds (OpenAPI + GraphQL drift). */
|
|
115
|
+
type ContractFindingKind = 'missing-operation' | 'undocumented-status' | 'response-schema' | 'graphql-syntax' | 'graphql-validation' | 'graphql-errors' | 'graphql-no-query' | 'request-body-schema' | 'missing-required-body' | 'undocumented-body' | 'unsupported-media-type' | 'missing-required-param' | 'param-schema' | 'undocumented-param' | 'graphql-variable-missing' | 'graphql-variable-invalid' | 'graphql-undocumented-variable';
|
|
116
|
+
/** A single contract discrepancy between a response and its declared shape. */
|
|
117
|
+
interface ContractFinding {
|
|
118
|
+
kind: ContractFindingKind;
|
|
119
|
+
message: string;
|
|
120
|
+
/** JSON Pointer / field path to the offending value, when applicable. */
|
|
121
|
+
path?: string;
|
|
122
|
+
severity: 'error' | 'warning';
|
|
123
|
+
}
|
|
124
|
+
/** Result of validating a response against a contract (OpenAPI/GraphQL). */
|
|
125
|
+
interface ContractResult {
|
|
126
|
+
valid: boolean;
|
|
127
|
+
findings: ContractFinding[];
|
|
128
|
+
/** Matched OpenAPI operation (lowercased method + path template), when found. */
|
|
129
|
+
operation?: {
|
|
130
|
+
method: string;
|
|
131
|
+
path: string;
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** Resolves named secrets at the transport boundary. */
|
|
135
|
+
interface SecretStore {
|
|
136
|
+
get(name: string): Promise<string | undefined>;
|
|
137
|
+
}
|
|
138
|
+
/** The request as prepared for the wire — headers/url here are REDACTED for
|
|
139
|
+
* agent-facing output (secret values never appear). */
|
|
140
|
+
interface PreparedRequest {
|
|
141
|
+
method: string;
|
|
142
|
+
url: string;
|
|
143
|
+
headers: Record<string, string>;
|
|
144
|
+
/** Materialized body (redacted), if any. */
|
|
145
|
+
body?: string;
|
|
146
|
+
}
|
|
147
|
+
interface RunResponse {
|
|
148
|
+
status: number;
|
|
149
|
+
latencyMs: number;
|
|
150
|
+
headers: Record<string, string>;
|
|
151
|
+
assertions: AssertionResult[];
|
|
152
|
+
/** Results of `test(...)` calls in the post-response script (if any). */
|
|
153
|
+
scriptTests: ScriptTest[];
|
|
154
|
+
captured: Record<string, unknown>;
|
|
155
|
+
/** Resource handle for the response body — never inlined. */
|
|
156
|
+
bodyHandle: string;
|
|
157
|
+
/** Redirect hops followed before the final response (redacted locations).
|
|
158
|
+
* Empty when no redirect was followed. */
|
|
159
|
+
redirects?: {
|
|
160
|
+
status: number;
|
|
161
|
+
location: string;
|
|
162
|
+
}[];
|
|
163
|
+
}
|
|
164
|
+
interface RunResult {
|
|
165
|
+
/** What was (or, for a dry-run, would be) sent — redacted. */
|
|
166
|
+
request: PreparedRequest;
|
|
167
|
+
/** Whether the request was actually dispatched. */
|
|
168
|
+
sent: boolean;
|
|
169
|
+
/** True when a mutating request was withheld (dry-run). */
|
|
170
|
+
dryRun: boolean;
|
|
171
|
+
/** Why it was withheld, when applicable. */
|
|
172
|
+
reason?: string;
|
|
173
|
+
/** Present only when `sent`. */
|
|
174
|
+
response?: RunResponse;
|
|
175
|
+
}
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/assert.d.ts
|
|
178
|
+
interface ResponseContext {
|
|
179
|
+
status: number;
|
|
180
|
+
statusText: string;
|
|
181
|
+
headers: Record<string, string>;
|
|
182
|
+
bodyText: string;
|
|
183
|
+
json: unknown;
|
|
184
|
+
latencyMs: number;
|
|
185
|
+
}
|
|
186
|
+
/** Evaluate declarative assertions against a response. */
|
|
187
|
+
declare function evaluateAssertions(specs: AssertionSpec[], ctx: ResponseContext): AssertionResult[];
|
|
188
|
+
/** Extract captured variables from a response into a name→value map. */
|
|
189
|
+
declare function extractCaptures(specs: CaptureSpec[], ctx: ResponseContext): Record<string, unknown>;
|
|
190
|
+
//#endregion
|
|
191
|
+
//#region src/collection.d.ts
|
|
192
|
+
/**
|
|
193
|
+
* Load a Bruno collection directory: each `<name>.bru` request (+ optional
|
|
194
|
+
* `<name>.sackville.yml` sidecar), plus any `environments/<Env>.bru` files.
|
|
195
|
+
*/
|
|
196
|
+
declare function loadCollection(dir: string): Collection;
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region src/contract.d.ts
|
|
199
|
+
interface OpenApiResponse {
|
|
200
|
+
content?: Record<string, {
|
|
201
|
+
schema?: unknown;
|
|
202
|
+
}>;
|
|
203
|
+
[key: string]: unknown;
|
|
204
|
+
}
|
|
205
|
+
interface OpenApiOperation {
|
|
206
|
+
responses?: Record<string, OpenApiResponse>;
|
|
207
|
+
[key: string]: unknown;
|
|
208
|
+
}
|
|
209
|
+
interface OpenApiDoc {
|
|
210
|
+
paths?: Record<string, Record<string, OpenApiOperation> | undefined>;
|
|
211
|
+
components?: {
|
|
212
|
+
schemas?: Record<string, unknown>;
|
|
213
|
+
[key: string]: unknown;
|
|
214
|
+
};
|
|
215
|
+
[key: string]: unknown;
|
|
216
|
+
}
|
|
217
|
+
interface ResponseFacts {
|
|
218
|
+
status: number;
|
|
219
|
+
headers?: Record<string, string>;
|
|
220
|
+
body: unknown;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Validate a response against an OpenAPI 3.1 document. Returns structured
|
|
224
|
+
* findings; `valid` is true only when no `error`-severity finding is present.
|
|
225
|
+
*/
|
|
226
|
+
interface OpenApiValidateOptions {
|
|
227
|
+
/** Directory the spec was loaded from — enables external local-file `$ref`
|
|
228
|
+
* resolution (relative refs resolve against it). Omit to disable. */
|
|
229
|
+
baseDir?: string;
|
|
230
|
+
}
|
|
231
|
+
/** A resolved OpenAPI operation: the operation object, its path template, and the
|
|
232
|
+
* raw path-item (its method keys plus any path-level `parameters`). */
|
|
233
|
+
interface ResolvedOperation {
|
|
234
|
+
operation: OpenApiOperation;
|
|
235
|
+
template: string;
|
|
236
|
+
pathItem: Record<string, unknown>;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Resolve `METHOD path` to its OpenAPI operation via path-template matching (shared
|
|
240
|
+
* by the response AND request validators — extracted so both resolve operations the
|
|
241
|
+
* same way). Returns `undefined` when no path template / method matches.
|
|
242
|
+
*/
|
|
243
|
+
declare function resolveOpenApiOperation(spec: OpenApiDoc, method: string, path: string): ResolvedOperation | undefined;
|
|
244
|
+
/**
|
|
245
|
+
* Normalize an OpenAPI schema for ajv (2020-12) and return a self-contained schema
|
|
246
|
+
* with component schemas merged into `$defs`. Shared by the response AND request
|
|
247
|
+
* (body + parameter) validators so every schema gets the same treatment: external
|
|
248
|
+
* local-file `$ref` inlining (when `baseDir` is set), the OpenAPI 3.0 `nullable`→
|
|
249
|
+
* type-union shim, and the `#/components/schemas/X`→`#/$defs/X` rewrite. A schema's
|
|
250
|
+
* own local `$defs` win over component defs on a name clash.
|
|
251
|
+
*/
|
|
252
|
+
declare function normalizeOpenApiSchema(schema: unknown, spec: OpenApiDoc, opts?: OpenApiValidateOptions): Record<string, unknown>;
|
|
253
|
+
declare function validateOpenApiResponse(spec: OpenApiDoc, req: {
|
|
254
|
+
method: string;
|
|
255
|
+
path: string;
|
|
256
|
+
}, res: ResponseFacts, opts?: OpenApiValidateOptions): ContractResult;
|
|
257
|
+
//#endregion
|
|
258
|
+
//#region src/graphql.d.ts
|
|
259
|
+
interface GraphqlValidateOptions {
|
|
260
|
+
/** Response payload to inspect for a top-level `errors` array. */
|
|
261
|
+
json?: unknown;
|
|
262
|
+
/** For a multi-operation document, scope the root-type drift check to this
|
|
263
|
+
* operation (and require it to exist). Without it, every operation is checked. */
|
|
264
|
+
operationName?: string;
|
|
265
|
+
/** The runtime variable values to validate against the operation's declared variable
|
|
266
|
+
* types (ADR 0015). Variable validation runs ONLY when this is provided (so existing
|
|
267
|
+
* query-vs-SDL-only callers are behavior-preserved). */
|
|
268
|
+
variables?: unknown;
|
|
269
|
+
/** Caller KNOWS the variable set is complete (direct surfaces hold the real request).
|
|
270
|
+
* Default false: an absent required variable is `unverified`, not a finding. */
|
|
271
|
+
variablesAuthoritative?: boolean;
|
|
272
|
+
/** Operator-supplied custom-scalar coercers, keyed by scalar name (ADR 0018). A registered
|
|
273
|
+
* scalar's VARIABLE values become checkable (the coercer throws on definite invalidity);
|
|
274
|
+
* document literals are NEVER routed through coercers (the `parseLiteral` leak guard, §3).
|
|
275
|
+
* Built-in scalar names are silently ignored (§8.5). Operator-set — never an agent input;
|
|
276
|
+
* the MCP surface selects coercers by NAME against an operator-bound registry. */
|
|
277
|
+
scalarCoercers?: Record<string, ScalarCoercer>;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* A custom-scalar coercer (ADR 0018). Throws (any error) to reject a value as definitely
|
|
281
|
+
* invalid; the return value is IGNORED — only throw/no-throw is the signal. MUST throw ONLY on
|
|
282
|
+
* definite invalidity (indeterminate ⇒ do not throw, so an uncertain value never false-fires).
|
|
283
|
+
* Applies to VARIABLE values only — document literals are never routed through a coercer.
|
|
284
|
+
*/
|
|
285
|
+
type ScalarCoercer = (value: unknown) => unknown;
|
|
286
|
+
interface GraphqlValidationResult extends ContractResult {
|
|
287
|
+
/** A variable set the validator could not check (custom-scalar-typed variables, a
|
|
288
|
+
* non-object `variables`, an ambiguous multi-operation target, or an absent required
|
|
289
|
+
* variable the caller is not authoritative about) — OR a custom-scalar directive-arg
|
|
290
|
+
* literal (ADR 0018 D2). Additive/optional — the verdict shape is UNCHANGED; the capture
|
|
291
|
+
* bridge folds this into `noSignal` so it can never become a pass (absence-is-never-a-pass).
|
|
292
|
+
* Omitted when everything relevant was verifiable. */
|
|
293
|
+
unverified?: boolean;
|
|
294
|
+
/** The `unverified` flag was (at least partly) caused by a custom-scalar directive-arg
|
|
295
|
+
* LITERAL (ADR 0018 D2), as distinct from an unverifiable variable. Additive/optional; lets
|
|
296
|
+
* the capture bridge bump the distinct `graphql-directive-unverified` summary key (ADR 0018
|
|
297
|
+
* §8.4) instead of mislabeling it `graphql-variable-unverified`. Omitted otherwise. */
|
|
298
|
+
directiveUnverified?: boolean;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Validate a GraphQL `query` against a schema `sdl`; if `opts.json` is supplied,
|
|
302
|
+
* also check the response payload for returned `errors`. With `opts.operationName`
|
|
303
|
+
* the root-type drift check is scoped to that operation (which must exist). When
|
|
304
|
+
* `opts.variables` is supplied, the runtime variable values are validated against the
|
|
305
|
+
* operation's declared types (ADR 0015). `valid` is true only when no `error`-severity
|
|
306
|
+
* finding is present.
|
|
307
|
+
*/
|
|
308
|
+
declare function validateGraphqlOperation(sdl: string, query: string, opts?: GraphqlValidateOptions): GraphqlValidationResult;
|
|
309
|
+
//#endregion
|
|
310
|
+
//#region src/har-capture.d.ts
|
|
311
|
+
/** A captured HTTP exchange reduced to the facts the validator needs. */
|
|
312
|
+
interface CaptureEntry {
|
|
313
|
+
req: {
|
|
314
|
+
method: string;
|
|
315
|
+
path: string;
|
|
316
|
+
origin: string;
|
|
317
|
+
/**
|
|
318
|
+
* The parsed JSON request body, when the request carried a JSON `postData`
|
|
319
|
+
* (attach `_file` first, inline `text` fallback). Needed for GraphQL drift:
|
|
320
|
+
* the operation `query` lives in the request, not the response (ADR 0013 §5).
|
|
321
|
+
*/
|
|
322
|
+
body?: unknown;
|
|
323
|
+
/** Decoded request query params (repeated keys → array). Captured for
|
|
324
|
+
* request-side contract validation (slice 4a); only set when non-empty. */
|
|
325
|
+
query?: Record<string, string | string[]>; /** Lower-cased request header names → value (last wins). Only set when present. */
|
|
326
|
+
headers?: Record<string, string>;
|
|
327
|
+
/** Decoded TEXT fields of a `form`-style request body (form-urlencoded /
|
|
328
|
+
* multipart text parts) from HAR `postData.params[]` (repeated keys → array;
|
|
329
|
+
* urlencoded `text` fallback). The authoritative-source rule still holds: the
|
|
330
|
+
* capture path drives the validator NON-authoritatively (ADR 0016 addendum 4). */
|
|
331
|
+
form?: Record<string, string | string[]>;
|
|
332
|
+
/** NAMES of multipart FILE parts (a `postData.params` entry with `fileName`);
|
|
333
|
+
* bytes never enter the facts. */
|
|
334
|
+
formFileFields?: string[];
|
|
335
|
+
};
|
|
336
|
+
res: ResponseFacts;
|
|
337
|
+
/** Lower-cased response content-type (sans parameters), e.g. `application/json`. */
|
|
338
|
+
mimeType: string;
|
|
339
|
+
/**
|
|
340
|
+
* Set when an attached body referenced by `_file` could not be resolved or
|
|
341
|
+
* JSON-parsed — surfaced as a HARD finding by the driver, never an empty-body
|
|
342
|
+
* pass (ADR 0013 slice 2).
|
|
343
|
+
*/
|
|
344
|
+
unresolvedBody?: string;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Slice 2 — parse a HAR `.zip` and resolve each entry's body. The PRIMARY path
|
|
348
|
+
* is `content:'attach'` (the only mode the browser pillar emits): a body lives
|
|
349
|
+
* in a separate archive entry referenced by `response.content._file`. Inline
|
|
350
|
+
* `response.content.text` (`content:'embed'`) is the fallback. JSON bodies are
|
|
351
|
+
* parsed; the URL is reduced to its `pathname` (+ origin kept separately).
|
|
352
|
+
*/
|
|
353
|
+
declare function harEntriesToFacts(harZip: Buffer): CaptureEntry[];
|
|
354
|
+
/** Slice 3 — only JSON responses from allowed origins are routed to the validator. */
|
|
355
|
+
interface CaptureFilterOptions {
|
|
356
|
+
/** When set, only entries whose request origin is in this list are considered. */
|
|
357
|
+
allowedOrigins?: string[];
|
|
358
|
+
}
|
|
359
|
+
interface OpenApiSpec {
|
|
360
|
+
servers?: Array<{
|
|
361
|
+
url?: string;
|
|
362
|
+
}>;
|
|
363
|
+
paths?: Record<string, Record<string, unknown> | undefined>;
|
|
364
|
+
[key: string]: unknown;
|
|
365
|
+
}
|
|
366
|
+
/** The GraphQL half of a capture contract (ADR 0013 §5 discriminated input). */
|
|
367
|
+
interface GraphqlContract {
|
|
368
|
+
/** The full request pathname that serves GraphQL, e.g. `/graphql`. */
|
|
369
|
+
endpointPath: string;
|
|
370
|
+
/** The schema SDL captured operations are validated against (drift detection). */
|
|
371
|
+
sdl: string;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* The discriminated contract a captured run is validated against. Supply
|
|
375
|
+
* `openapi` (REST), `graphql` (GraphQL), or both. A captured entry is routed to
|
|
376
|
+
* exactly one validator; a GraphQL entry never falls through to the OpenAPI
|
|
377
|
+
* validator (which would flood `missing-operation`), and an entry with no
|
|
378
|
+
* matching contract half is **no-signal**, never a pass (ADR 0013 §1/§5).
|
|
379
|
+
*/
|
|
380
|
+
interface CaptureContract {
|
|
381
|
+
openapi?: OpenApiSpec;
|
|
382
|
+
graphql?: GraphqlContract;
|
|
383
|
+
}
|
|
384
|
+
/** The rolled-up contract sub-verdict over a captured run (ADR 0013 §1/§5). */
|
|
385
|
+
interface CaptureContractVerdict {
|
|
386
|
+
/** Entries actually routed to the validator (JSON, allowed origin). */
|
|
387
|
+
entriesValidated: number;
|
|
388
|
+
/** Per-`ContractFindingKind` finding counts across all entries (+ `unresolved-body`). */
|
|
389
|
+
findingsByKind: Record<string, number>;
|
|
390
|
+
/** The first entry that failed validation, for a fast headline. */
|
|
391
|
+
firstFailing?: {
|
|
392
|
+
method: string;
|
|
393
|
+
path: string;
|
|
394
|
+
kind: string;
|
|
395
|
+
message: string;
|
|
396
|
+
};
|
|
397
|
+
/** `METHOD /template` for every documented op an entry exercised — the inverse-of-drift signal. */
|
|
398
|
+
exercisedOperations: string[];
|
|
399
|
+
/** Documented ops NO captured entry hit. */
|
|
400
|
+
unexercisedOperations: string[];
|
|
401
|
+
/** Per-entry validator results (finding messages already redacted). */
|
|
402
|
+
results: ContractResult[];
|
|
403
|
+
/** Entries whose attached body could not be resolved/parsed (never a pass). */
|
|
404
|
+
unresolvedBodies: number;
|
|
405
|
+
/**
|
|
406
|
+
* Entries we could NOT verify because no matching contract half was supplied —
|
|
407
|
+
* a GraphQL call with no SDL (`graphql-sdl-not-supplied`), or a REST call with
|
|
408
|
+
* no OpenAPI spec (`no-contract-for-entry`). Counted, never a pass (ADR 0013 §1).
|
|
409
|
+
*/
|
|
410
|
+
noSignal: number;
|
|
411
|
+
/**
|
|
412
|
+
* `true` only when at least one entry was validated, every result is valid, no
|
|
413
|
+
* body was unresolved, AND no entry was no-signal. Absence is never a pass
|
|
414
|
+
* (ADR 0013 §1) — an unverifiable GraphQL/REST call can't make a run clean.
|
|
415
|
+
*/
|
|
416
|
+
clean: boolean;
|
|
417
|
+
}
|
|
418
|
+
interface ValidateCaptureOptions extends CaptureFilterOptions {
|
|
419
|
+
/** Redacts finding messages before they leave the bridge (ADR 0013 §3b). */
|
|
420
|
+
redact?: (value: string) => string;
|
|
421
|
+
/** Spec dir for external local-file `$ref` resolution (passed to the validator). */
|
|
422
|
+
baseDir?: string;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Slice 5 (+ ADR 0013 §5 GraphQL) — drive each captured JSON entry through the
|
|
426
|
+
* matching shipped validator. A GraphQL entry (matched by the contract's
|
|
427
|
+
* `endpointPath` or the JSON `{query}` shape) goes to `validateGraphqlOperation`
|
|
428
|
+
* and NEVER to the OpenAPI validator; a REST entry goes to
|
|
429
|
+
* `validateOpenApiResponse` and feeds the exercised/unexercised drift walk. An
|
|
430
|
+
* entry with no matching contract half is no-signal (never a pass). Every finding
|
|
431
|
+
* message is routed through the operator `Redactor`; our own summary paths use the
|
|
432
|
+
* matched operation template / operator-supplied endpoint, never a raw captured path.
|
|
433
|
+
*/
|
|
434
|
+
declare function validateCapturedTraffic(harZip: Buffer, contract: CaptureContract, opts?: ValidateCaptureOptions): CaptureContractVerdict;
|
|
435
|
+
//#endregion
|
|
436
|
+
//#region src/har-synth.d.ts
|
|
437
|
+
/** Compact tallies over a parsed HAR; tolerant of a malformed/empty log. */
|
|
438
|
+
interface HarCounts {
|
|
439
|
+
entryCount: number;
|
|
440
|
+
byStatus: Record<string, number>;
|
|
441
|
+
byMethod: Record<string, number>;
|
|
442
|
+
}
|
|
443
|
+
/** Parse the `.har` JSON into compact tallies; tolerant of a malformed/empty log. */
|
|
444
|
+
declare function summarizeHar(harJson: string): HarCounts;
|
|
445
|
+
/**
|
|
446
|
+
* The blanket-redaction pass over a HAR `.zip` Buffer (PURE — no file I/O). Unzips,
|
|
447
|
+
* collects the text-like attach `_file` bodies by DECLARED mimeType (so a body stored
|
|
448
|
+
* under a non-text filename is still scrubbed), redacts every text member — the `.har`
|
|
449
|
+
* JSON + text-extension bodies + those attach bodies — and re-zips. A genuinely binary
|
|
450
|
+
* member passes through byte-for-byte. Shared by the browser pillar's `finalizeHar`
|
|
451
|
+
* (file wrapper) and api synthesis (in-memory), so they share ONE redaction code path.
|
|
452
|
+
*/
|
|
453
|
+
declare function redactHarZip(zip: Buffer, redact: (value: string) => string): Buffer;
|
|
454
|
+
/**
|
|
455
|
+
* One captured HTTP exchange (a single request OR a single redirect hop) reduced to
|
|
456
|
+
* what {@link synthesizeRedactedHarZip} needs. The runner's produce channel
|
|
457
|
+
* (`runRequestForHar`, slice 4) emits one of these per hop — so a redirect chain
|
|
458
|
+
* becomes one HAR entry per hop, never a collapsed chain (ADR 0013 Addendum 4 gap a).
|
|
459
|
+
* Bodies are STRING-only: a Buffer/FormData (file/multipart) request body leaves
|
|
460
|
+
* `reqBody` undefined ⇒ no `postData` is synthesized (lossless — the response still
|
|
461
|
+
* validates; gap b's GraphQL path needs only string JSON bodies anyway).
|
|
462
|
+
*/
|
|
463
|
+
interface HarHopRecord {
|
|
464
|
+
method: string;
|
|
465
|
+
url: string;
|
|
466
|
+
/** The real numeric HTTP status. A missing status is INCOMPLETE capture and THROWS
|
|
467
|
+
* (never coerced to 0, which the bridge would read as an undocumented-status finding —
|
|
468
|
+
* a false fail masking the true inconclusive state). */
|
|
469
|
+
status: number;
|
|
470
|
+
resContentType?: string;
|
|
471
|
+
/** The real (secret-bearing) response body text; redacted by {@link redactHarZip}. */
|
|
472
|
+
resBody?: string;
|
|
473
|
+
reqContentType?: string;
|
|
474
|
+
/** The real (secret-bearing) request body text (GraphQL's `query` lives here). */
|
|
475
|
+
reqBody?: string;
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Synthesize a HAR `.zip` Buffer from per-hop records and IMMEDIATELY redact it — the
|
|
479
|
+
* ONLY public surface, so no un-redacted synthesized buffer is ever returned/stored/
|
|
480
|
+
* validated (ADR 0013 §3b). Builds `{log:{entries}}` with ONLY the six fields the
|
|
481
|
+
* consume bridge reads, INLINE `text` bodies (we hold the strings — no `_file` attach),
|
|
482
|
+
* one `.har` member, then runs {@link redactHarZip}. THROWS on a status-less record.
|
|
483
|
+
*/
|
|
484
|
+
declare function synthesizeRedactedHarZip(records: HarHopRecord[], redact: (value: string) => string): Buffer;
|
|
485
|
+
//#endregion
|
|
486
|
+
//#region src/request-contract.d.ts
|
|
487
|
+
/** The request facts a validator reads. `path` is the pathname only (matches
|
|
488
|
+
* `CaptureEntry.req.path`); `body` is the already-parsed JSON body. */
|
|
489
|
+
interface RequestFacts {
|
|
490
|
+
method: string;
|
|
491
|
+
path: string;
|
|
492
|
+
body?: unknown;
|
|
493
|
+
query?: Record<string, string | string[]>;
|
|
494
|
+
/** Lower-cased header names → value. */
|
|
495
|
+
headers?: Record<string, string>;
|
|
496
|
+
/** Decoded fields of a `form`-style body (`application/x-www-form-urlencoded` or the
|
|
497
|
+
* text parts of `multipart/form-data`); repeated keys → array. The AUTHORITATIVE
|
|
498
|
+
* structured channel for non-JSON body validation (ADR 0016 addendum 4) — populated at
|
|
499
|
+
* prepare time from the structured parts, NEVER by re-parsing the serialized string.
|
|
500
|
+
* File-part bytes never enter this map. */
|
|
501
|
+
form?: Record<string, string | string[]>;
|
|
502
|
+
/** Field NAMES of `multipart/form-data` FILE parts (bytes never inlined — redaction).
|
|
503
|
+
* A declared schema property satisfied by a file part is `unverified`-skipped. */
|
|
504
|
+
formFileFields?: string[];
|
|
505
|
+
}
|
|
506
|
+
interface OpenApiRequestValidateOptions extends OpenApiValidateOptions {
|
|
507
|
+
/** Caller KNOWS body presence/absence is authoritative (direct surfaces). Default
|
|
508
|
+
* false: an absent required body is `unverified`, not `missing-required-body`. */
|
|
509
|
+
bodyPresenceAuthoritative?: boolean;
|
|
510
|
+
/** Caller KNOWS query/header facts are complete (direct surfaces). Default false:
|
|
511
|
+
* an absent required query/header param is `unverified`, not a finding. */
|
|
512
|
+
paramsAuthoritative?: boolean;
|
|
513
|
+
}
|
|
514
|
+
interface RequestValidationResult extends ContractResult {
|
|
515
|
+
/** A body/param the validator could not verify and the caller is not authoritative
|
|
516
|
+
* about. The capture bridge folds this into `noSignal` (never a finding, never a
|
|
517
|
+
* pass). Omitted when everything relevant was verifiable. */
|
|
518
|
+
unverified?: boolean;
|
|
519
|
+
}
|
|
520
|
+
/** True when a parsed body looks like a GraphQL-over-HTTP envelope (`{query: string,
|
|
521
|
+
* …}`). A direct surface uses this to refuse running OpenAPI body validation on a
|
|
522
|
+
* GraphQL request (which has no REST requestBody shape) — H4. */
|
|
523
|
+
declare function isGraphqlEnvelope(body: unknown): boolean;
|
|
524
|
+
declare function validateOpenApiRequest(spec: OpenApiDoc, req: RequestFacts, opts?: OpenApiRequestValidateOptions): RequestValidationResult;
|
|
525
|
+
//#endregion
|
|
526
|
+
//#region src/runner.d.ts
|
|
527
|
+
interface RunOptions {
|
|
528
|
+
/** Variable scope for `{{var}}` interpolation. */
|
|
529
|
+
vars?: Record<string, unknown>;
|
|
530
|
+
/** Name of a collection environment whose vars seed the scope (lowest precedence). */
|
|
531
|
+
env?: string;
|
|
532
|
+
/** Secret store for `{{secret:NAME}}` resolution (defaults to env). */
|
|
533
|
+
secrets?: SecretStore;
|
|
534
|
+
/** Artifact store for the response body (a fresh one is used if omitted). */
|
|
535
|
+
artifacts?: ArtifactStore;
|
|
536
|
+
/** Opt in to actually sending mutating requests. */
|
|
537
|
+
allowUnsafe?: boolean;
|
|
538
|
+
/** Hostnames a mutating request may reach. */
|
|
539
|
+
allowedHosts?: string[];
|
|
540
|
+
/** Permit loopback/private SSRF targets (default true; see `assertSsrfAllowed`).
|
|
541
|
+
* Set false to block all private ranges, not just metadata/link-local. */
|
|
542
|
+
allowPrivate?: boolean;
|
|
543
|
+
/** Injectable DNS resolver for the SSRF pre-flight (tests). */
|
|
544
|
+
lookup?: DnsLookup;
|
|
545
|
+
/** Maximum redirect hops to follow (default 0 = don't follow; return the 3xx).
|
|
546
|
+
* Every hop is re-checked: SSRF range-block + the mutation host-allowlist. */
|
|
547
|
+
maxRedirects?: number;
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* The PRODUCE-only out-of-band capture channel (ADR 0013 Addendum 4, 5f): the raw
|
|
551
|
+
* per-hop request/response facts a synthesized HAR is built from, plus the run-resolved
|
|
552
|
+
* secret pairs the union redactor must learn. NEVER attached to `RunResult` — raw bodies
|
|
553
|
+
* stay structurally off anything an agent sees; only `runRequestForHar` exposes it, and
|
|
554
|
+
* its consumer ({@link synthesizeRedactedHarZip} via the verify driver) redacts before
|
|
555
|
+
* store + validate.
|
|
556
|
+
*/
|
|
557
|
+
interface HarCapture {
|
|
558
|
+
hops: HarHopRecord[];
|
|
559
|
+
registeredSecrets: {
|
|
560
|
+
name: string;
|
|
561
|
+
value: string;
|
|
562
|
+
}[];
|
|
563
|
+
/** The terminal response was still a 3xx (budget exhausted / unparseable / missing
|
|
564
|
+
* Location): the exchange did not complete to a resource ⇒ the driver throws
|
|
565
|
+
* (inconclusive), never validates a truncated chain. */
|
|
566
|
+
redirectTruncated: boolean;
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Execute one request: resolve vars + secrets, apply the mutation safety gate
|
|
570
|
+
* (mutating methods dry-run unless explicitly unlocked), dispatch via undici if
|
|
571
|
+
* allowed, evaluate assertions, and return a result whose surfaced strings
|
|
572
|
+
* (request, response headers, body artifact) are all secret-redacted.
|
|
573
|
+
*/
|
|
574
|
+
declare function runRequest(collection: Collection, name: string, opts?: RunOptions): Promise<RunResult>;
|
|
575
|
+
/**
|
|
576
|
+
* Drive a request AND retain the raw per-hop facts a synthesized HAR is built from
|
|
577
|
+
* (ADR 0013 Addendum 4, 5f) — the verify-driven api capture path. `RunResult` is
|
|
578
|
+
* returned UNCHANGED (still fully redacted); `capture` is the produce-only raw channel
|
|
579
|
+
* (never on `RunResult`). The caller (verify driver) folds `capture.registeredSecrets`
|
|
580
|
+
* into a union redactor, then synthesizes + redacts + validates.
|
|
581
|
+
*/
|
|
582
|
+
declare function runRequestForHar(collection: Collection, name: string, opts?: RunOptions): Promise<{
|
|
583
|
+
result: RunResult;
|
|
584
|
+
capture: HarCapture;
|
|
585
|
+
}>;
|
|
586
|
+
/**
|
|
587
|
+
* The out-of-band channel surfacing the UN-redacted {@link RequestFacts} a contract
|
|
588
|
+
* validator reads (method, pathname, decoded query, lower-cased headers, JSON-parsed
|
|
589
|
+
* body), plus the run-resolved secret pairs a downstream redactor must learn to scrub
|
|
590
|
+
* findings. Mirrors {@link HarCapture}: NEVER attached to `RunResult` (which stays fully
|
|
591
|
+
* redacted), and populated at PREPARE time so it is available even when the request is
|
|
592
|
+
* withheld (dry-run / gated). Consumed by `api run --openapi`'s request-side validation
|
|
593
|
+
* (ADR 0014).
|
|
594
|
+
*/
|
|
595
|
+
interface ContractRequestCapture {
|
|
596
|
+
request: RequestFacts;
|
|
597
|
+
registeredSecrets: {
|
|
598
|
+
name: string;
|
|
599
|
+
value: string;
|
|
600
|
+
}[];
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Drive a request AND retain the un-redacted request facts a contract validator reads
|
|
604
|
+
* (ADR 0014). `RunResult` is returned UNCHANGED (still fully redacted); `capture` is the
|
|
605
|
+
* raw channel (never on `RunResult`). The caller (`api run --openapi`) folds
|
|
606
|
+
* `capture.registeredSecrets` into a redactor, validates `capture.request` against the
|
|
607
|
+
* contract, then redacts the findings before surfacing them.
|
|
608
|
+
*/
|
|
609
|
+
declare function runRequestForContract(collection: Collection, name: string, opts?: RunOptions): Promise<{
|
|
610
|
+
result: RunResult;
|
|
611
|
+
capture: ContractRequestCapture;
|
|
612
|
+
}>;
|
|
613
|
+
//#endregion
|
|
614
|
+
//#region src/secrets.d.ts
|
|
615
|
+
/** In-memory store (tests / explicit injection). */
|
|
616
|
+
declare class StaticSecretStore implements SecretStore {
|
|
617
|
+
private readonly values;
|
|
618
|
+
constructor(values?: Record<string, string>);
|
|
619
|
+
get(name: string): Promise<string | undefined>;
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Reads `<prefix><NAME>` from the environment — the zero-dependency default
|
|
623
|
+
* (Linux/CI). The default prefix is `SACKVILLE_SECRET_`; the aggregate server
|
|
624
|
+
* overrides it to `SACKVILLE_API_SECRET_` so the api pillar's secrets live in its
|
|
625
|
+
* own namespace and can't be read via a bare, shared name (ADR 0019).
|
|
626
|
+
*/
|
|
627
|
+
declare class EnvSecretStore implements SecretStore {
|
|
628
|
+
private readonly env;
|
|
629
|
+
private readonly prefix;
|
|
630
|
+
constructor(env?: Record<string, string | undefined>, prefix?: string);
|
|
631
|
+
get(name: string): Promise<string | undefined>;
|
|
632
|
+
}
|
|
633
|
+
/** OS keychain via @napi-rs/keyring (macOS/Windows/Linux-desktop). The native
|
|
634
|
+
* module is loaded lazily through a non-literal specifier so importing this file
|
|
635
|
+
* never loads it; Secret Service can throw at runtime in headless containers, so
|
|
636
|
+
* failures resolve to undefined. */
|
|
637
|
+
declare class KeyringSecretStore implements SecretStore {
|
|
638
|
+
private readonly service;
|
|
639
|
+
constructor(service?: string);
|
|
640
|
+
get(name: string): Promise<string | undefined>;
|
|
641
|
+
}
|
|
642
|
+
/** Try each store in order, first hit wins. */
|
|
643
|
+
declare class ChainedSecretStore implements SecretStore {
|
|
644
|
+
private readonly stores;
|
|
645
|
+
constructor(stores: SecretStore[]);
|
|
646
|
+
get(name: string): Promise<string | undefined>;
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Default store: keyring (opt-in) chained ahead of env, else env only.
|
|
650
|
+
* `env`/`envPrefix` override the environment source + variable prefix the
|
|
651
|
+
* `EnvSecretStore` reads (the aggregate server passes `SACKVILLE_API_SECRET_`).
|
|
652
|
+
*/
|
|
653
|
+
declare function resolveSecretStore(opts?: {
|
|
654
|
+
keyring?: boolean;
|
|
655
|
+
env?: Record<string, string | undefined>;
|
|
656
|
+
envPrefix?: string;
|
|
657
|
+
}): SecretStore;
|
|
658
|
+
//#endregion
|
|
659
|
+
//#region src/sequence.d.ts
|
|
660
|
+
interface SequenceOptions extends RunOptions {
|
|
661
|
+
/** Stop the sequence if a request's assertions fail. */
|
|
662
|
+
stopOnFailure?: boolean;
|
|
663
|
+
}
|
|
664
|
+
interface SequenceStep {
|
|
665
|
+
name: string;
|
|
666
|
+
result: RunResult;
|
|
667
|
+
}
|
|
668
|
+
interface SequenceResult {
|
|
669
|
+
steps: SequenceStep[];
|
|
670
|
+
/** Variables captured across the whole sequence (threaded forward). */
|
|
671
|
+
captured: Record<string, unknown>;
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Run requests in order, threading each response's captured variables into the
|
|
675
|
+
* scope of the requests that follow (request chaining). Per-run options
|
|
676
|
+
* (secrets, allowUnsafe, allowlist, artifacts) apply to every request; the
|
|
677
|
+
* variable scope is the shared, accumulating one.
|
|
678
|
+
*/
|
|
679
|
+
declare function runSequence(collection: Collection, names: string[], opts?: SequenceOptions): Promise<SequenceResult>;
|
|
680
|
+
/**
|
|
681
|
+
* Like {@link runSequence} but ALSO retains the raw per-hop capture across every step
|
|
682
|
+
* (ADR 0013 Addendum 4, 5f) — for the verify-driven api capture path. Each step's
|
|
683
|
+
* `SequenceStep.result` is UNCHANGED (redacted); `capture` aggregates all hops + the
|
|
684
|
+
* union of run-resolved secret pairs. The transport-completeness guard (every
|
|
685
|
+
* `step.result.sent`) lives in the driver, which folds a non-sent step to inconclusive.
|
|
686
|
+
*/
|
|
687
|
+
declare function runSequenceForHar(collection: Collection, names: string[], opts?: SequenceOptions): Promise<{
|
|
688
|
+
result: SequenceResult;
|
|
689
|
+
capture: HarCapture;
|
|
690
|
+
}>;
|
|
691
|
+
//#endregion
|
|
692
|
+
//#region src/har-produce.d.ts
|
|
693
|
+
/** The minimal artifact store the driver writes the redacted HAR to — satisfied by the
|
|
694
|
+
* verify-prefix `@sackville-mcp/artifacts` `ArtifactStore` (kept structural so `@sackville-mcp/api`
|
|
695
|
+
* needn't depend on `@sackville-mcp/artifacts`). */
|
|
696
|
+
interface HarArtifactSink {
|
|
697
|
+
put(runId: string, kind: string, body: string | Buffer, contentType: string): string;
|
|
698
|
+
}
|
|
699
|
+
/** Compact HAR summary (shape-compatible with `@sackville-mcp/browser` `HarSummary`). */
|
|
700
|
+
interface ProducedHarSummary {
|
|
701
|
+
handle: string;
|
|
702
|
+
byteSize: number;
|
|
703
|
+
entryCount: number;
|
|
704
|
+
byStatus: Record<string, number>;
|
|
705
|
+
byMethod: Record<string, number>;
|
|
706
|
+
}
|
|
707
|
+
interface ProducedHar {
|
|
708
|
+
/** `<store-prefix>://<id>/har` — the redacted, stored HAR archive, by handle. */
|
|
709
|
+
harHandle: string;
|
|
710
|
+
summary: ProducedHarSummary;
|
|
711
|
+
/** The FULL contract verdict (incl. `clean`/`noSignal`/`unresolvedBodies`) so the
|
|
712
|
+
* downstream fold can hold "absence is never a pass" on the contract dimension. */
|
|
713
|
+
verdict: CaptureContractVerdict;
|
|
714
|
+
}
|
|
715
|
+
interface HarProduceDeps {
|
|
716
|
+
store: HarArtifactSink;
|
|
717
|
+
/** The union redactor (verify ∪ api seed); the driver folds in the run-resolved
|
|
718
|
+
* secrets and uses it at BOTH chokepoints (synthesize + validate). */
|
|
719
|
+
redactor: Redactor;
|
|
720
|
+
contract: CaptureContract;
|
|
721
|
+
validate?: {
|
|
722
|
+
baseDir?: string;
|
|
723
|
+
allowedOrigins?: string[];
|
|
724
|
+
};
|
|
725
|
+
idFactory?: () => string;
|
|
726
|
+
/** Injected so the gate suite needn't fetch. */
|
|
727
|
+
runForHar?: typeof runRequestForHar;
|
|
728
|
+
runSequenceForHar?: typeof runSequenceForHar;
|
|
729
|
+
}
|
|
730
|
+
/** Drive ONE request → synthesize + validate its HAR. Throws (⇒ inconclusive) when the
|
|
731
|
+
* request was not sent (withheld/dry-run/blocked) or its redirect chain was truncated. */
|
|
732
|
+
declare function runRequestToHar(collection: Collection, name: string, opts: RunOptions, deps: HarProduceDeps): Promise<ProducedHar>;
|
|
733
|
+
/** Drive a SEQUENCE → synthesize + validate the aggregated HAR. Throws (⇒ inconclusive)
|
|
734
|
+
* when ANY step was not sent (per `step.result.sent`) or any hop truncated a redirect. */
|
|
735
|
+
declare function runSequenceToHar(collection: Collection, names: string[], opts: SequenceOptions, deps: HarProduceDeps): Promise<ProducedHar>;
|
|
736
|
+
//#endregion
|
|
737
|
+
//#region src/import.d.ts
|
|
738
|
+
type ImportFormat = 'postman' | 'insomnia' | 'openapi' | 'har';
|
|
739
|
+
/** Normalized request, format-agnostic. */
|
|
740
|
+
interface ImportedRequest {
|
|
741
|
+
name: string;
|
|
742
|
+
method: string;
|
|
743
|
+
url: string;
|
|
744
|
+
headers: {
|
|
745
|
+
name: string;
|
|
746
|
+
value: string;
|
|
747
|
+
}[];
|
|
748
|
+
body?: ImportedBody;
|
|
749
|
+
}
|
|
750
|
+
interface ImportedBody {
|
|
751
|
+
/** Canonical body type: json | text | xml | form-urlencoded | graphql. */
|
|
752
|
+
type: string;
|
|
753
|
+
/** Raw content (json/text/xml). */
|
|
754
|
+
content?: string;
|
|
755
|
+
/** form-urlencoded params. */
|
|
756
|
+
params?: {
|
|
757
|
+
name: string;
|
|
758
|
+
value: string;
|
|
759
|
+
}[];
|
|
760
|
+
/** graphql query + variables. */
|
|
761
|
+
graphql?: {
|
|
762
|
+
query: string;
|
|
763
|
+
variables?: string;
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
interface ImportResult {
|
|
767
|
+
requests: ImportedRequest[];
|
|
768
|
+
/** A discovered environment (e.g. OpenAPI `servers[0]`), if any. */
|
|
769
|
+
environment?: {
|
|
770
|
+
name: string;
|
|
771
|
+
variables: Record<string, string>;
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
/** Postman v2.1 collection → requests (folders flattened, depth-first). */
|
|
775
|
+
declare function importPostman(doc: unknown): ImportResult;
|
|
776
|
+
/** Insomnia v4 export → requests. */
|
|
777
|
+
declare function importInsomnia(doc: unknown): ImportResult;
|
|
778
|
+
/** OpenAPI 3.x → one request per operation + an environment for the server URL. */
|
|
779
|
+
declare function importOpenApi(doc: unknown): ImportResult;
|
|
780
|
+
/** HAR → one request per logged entry. */
|
|
781
|
+
declare function importHar(doc: unknown): ImportResult;
|
|
782
|
+
/** Parse a source document (JSON, or YAML for OpenAPI) into normalized requests. */
|
|
783
|
+
declare function parseImport(format: ImportFormat, source: string): ImportResult;
|
|
784
|
+
interface WriteOptions {
|
|
785
|
+
/** Collection name written to `bruno.json`. */
|
|
786
|
+
name?: string;
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Write a normalized import to `destDir` as a Bruno collection: `bruno.json`, a
|
|
790
|
+
* `<name>.bru` per request, and (if present) an `environments/<env>.bru`. Returns
|
|
791
|
+
* the request count written.
|
|
792
|
+
*/
|
|
793
|
+
declare function writeImported(destDir: string, result: ImportResult, opts?: WriteOptions): number;
|
|
794
|
+
/** One-shot: parse a source document and write the Bruno collection to disk. */
|
|
795
|
+
declare function importToCollection(format: ImportFormat, source: string, destDir: string, opts?: WriteOptions): number;
|
|
796
|
+
//#endregion
|
|
797
|
+
//#region src/prepare.d.ts
|
|
798
|
+
interface PreparedBody {
|
|
799
|
+
/** Content-Type to set when the request carries none; `undefined` lets undici
|
|
800
|
+
* set it itself (e.g. multipart, which needs a generated boundary). */
|
|
801
|
+
contentType?: string;
|
|
802
|
+
/** The payload handed to undici. */
|
|
803
|
+
content: string | Buffer | FormData;
|
|
804
|
+
/** A redaction-safe textual rendering of the body for agent-facing output
|
|
805
|
+
* (binary/file content is summarized, never inlined). */
|
|
806
|
+
preview: string;
|
|
807
|
+
/** For `form`-style bodies (form-urlencoded / multipart): the decoded TEXT fields
|
|
808
|
+
* (repeated keys → array) — the AUTHORITATIVE structured channel for non-JSON body
|
|
809
|
+
* contract validation (ADR 0016 addendum 4). Built from the structured parts at
|
|
810
|
+
* prepare time, NEVER by re-parsing `content`; file bytes never enter it. */
|
|
811
|
+
formFields?: Record<string, string | string[]>;
|
|
812
|
+
/** For multipart bodies: the NAMES of FILE parts (bytes are never inlined). */
|
|
813
|
+
formFileFields?: string[];
|
|
814
|
+
}
|
|
815
|
+
/** A request prepared for the wire — these strings carry REAL secret values. */
|
|
816
|
+
interface Prepared {
|
|
817
|
+
method: string;
|
|
818
|
+
url: string;
|
|
819
|
+
headers: Record<string, string>;
|
|
820
|
+
body?: PreparedBody;
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Resolve `{{secret:NAME}}` (from the store, registered with the redactor) and
|
|
824
|
+
* `{{var}}` (from the scope) across the URL, headers, AND body into the actual
|
|
825
|
+
* values sent on the wire. Fails closed on an unresolved secret.
|
|
826
|
+
*/
|
|
827
|
+
declare function prepareRequest(request: ApiRequest, scope: Record<string, unknown>, secrets: SecretStore, redactor: Redactor, baseDir?: string): Promise<Prepared>;
|
|
828
|
+
//#endregion
|
|
829
|
+
//#region src/safety.d.ts
|
|
830
|
+
interface SafetyOptions {
|
|
831
|
+
/** Opt in to actually sending mutating requests. */
|
|
832
|
+
allowUnsafe?: boolean;
|
|
833
|
+
/** Hostnames a mutating request is permitted to reach. */
|
|
834
|
+
allowedHosts?: string[];
|
|
835
|
+
}
|
|
836
|
+
interface SafetyDecision {
|
|
837
|
+
allowed: boolean;
|
|
838
|
+
reason: string;
|
|
839
|
+
}
|
|
840
|
+
declare function isMutating(method: string): boolean;
|
|
841
|
+
/**
|
|
842
|
+
* Decide whether a request may actually be sent. Safe methods always may.
|
|
843
|
+
* Mutating methods are withheld (dry-run) unless explicitly unlocked
|
|
844
|
+
* (`allowUnsafe`) AND the target host is on the allowlist — never silently fired.
|
|
845
|
+
*/
|
|
846
|
+
declare function checkGate(method: string, host: string, opts?: SafetyOptions): SafetyDecision;
|
|
847
|
+
//#endregion
|
|
848
|
+
//#region src/schema.d.ts
|
|
849
|
+
/** A single schema violation, located by JSON Pointer into the instance. */
|
|
850
|
+
interface SchemaError {
|
|
851
|
+
/** JSON Pointer to the offending value ('' = document root). */
|
|
852
|
+
instancePath: string;
|
|
853
|
+
message: string;
|
|
854
|
+
}
|
|
855
|
+
interface SchemaValidation {
|
|
856
|
+
valid: boolean;
|
|
857
|
+
errors: SchemaError[];
|
|
858
|
+
}
|
|
859
|
+
/** Validate `data` against a JSON Schema. Never throws on data; an invalid
|
|
860
|
+
* *schema* surfaces as a single error rather than propagating. */
|
|
861
|
+
declare function validateSchema(schema: unknown, data: unknown): SchemaValidation;
|
|
862
|
+
//#endregion
|
|
863
|
+
//#region src/script.d.ts
|
|
864
|
+
interface ScriptResponseView {
|
|
865
|
+
status: number;
|
|
866
|
+
headers: Record<string, string>;
|
|
867
|
+
body: string;
|
|
868
|
+
json: unknown;
|
|
869
|
+
}
|
|
870
|
+
interface ScriptResult {
|
|
871
|
+
/** The full variable scope after the script ran (incl. `bru.setVar` writes). */
|
|
872
|
+
vars: Record<string, unknown>;
|
|
873
|
+
tests: ScriptTest[];
|
|
874
|
+
logs: string[];
|
|
875
|
+
/** A top-level (non-`test`) error thrown by the script, if any. */
|
|
876
|
+
error?: string;
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Run a pre/post-request script in a QuickJS WASM sandbox with the curated
|
|
880
|
+
* `bru`/`expect`/`test`/`console` API and a wall-clock interrupt. The script
|
|
881
|
+
* sees `res` (post-response only) and reads/writes variables via `bru`; nothing
|
|
882
|
+
* from the host process is reachable.
|
|
883
|
+
*/
|
|
884
|
+
declare function runScript(code: string, context: {
|
|
885
|
+
vars: Record<string, unknown>;
|
|
886
|
+
res?: ScriptResponseView;
|
|
887
|
+
}): Promise<ScriptResult>;
|
|
888
|
+
//#endregion
|
|
889
|
+
//#region src/vars.d.ts
|
|
890
|
+
/**
|
|
891
|
+
* Interpolate `{{name}}` placeholders from a variable scope. Unknown names are
|
|
892
|
+
* left intact (so callers can detect them). Secret resolution is layered on
|
|
893
|
+
* later, at the transport boundary.
|
|
894
|
+
*/
|
|
895
|
+
declare function interpolate(template: string, scope: Record<string, unknown>): string;
|
|
896
|
+
//#endregion
|
|
897
|
+
export { type ApiRequest, type Artifact, ArtifactStore, type AssertionOp, type AssertionResult, type AssertionSource, type AssertionSpec, type CaptureContract, type CaptureContractVerdict, type CaptureEntry, type CaptureFilterOptions, type CaptureSpec, ChainedSecretStore, type Collection, type ContractFinding, type ContractFindingKind, type ContractRequestCapture, type ContractResult, EnvSecretStore, type GraphqlContract, type GraphqlValidateOptions, type GraphqlValidationResult, type HarArtifactSink, type HarCapture, type HarCounts, type HarHopRecord, type HarProduceDeps, type ImportFormat, type ImportResult, type ImportedRequest, KeyringSecretStore, type OpenApiDoc, type OpenApiRequestValidateOptions, type OpenApiValidateOptions, type Prepared, type PreparedBody, type PreparedRequest, type ProducedHar, type ProducedHarSummary, Redactor, type RequestBody, type RequestEntry, type RequestFacts, type RequestValidationResult, type ResolvedOperation, type ResponseContext, type ResponseFacts, type RunOptions, type RunResponse, type RunResult, type SafetyDecision, type SafetyOptions, type ScalarCoercer, type SchemaError, type SchemaValidation, type ScriptResponseView, type ScriptResult, type ScriptTest, type SecretStore, type SequenceOptions, type SequenceResult, type SequenceStep, StaticSecretStore, type ValidateCaptureOptions, checkGate, evaluateAssertions, extractCaptures, harEntriesToFacts, importHar, importInsomnia, importOpenApi, importPostman, importToCollection, interpolate, isGraphqlEnvelope, isMutating, loadCollection, normalizeOpenApiSchema, parseImport, prepareRequest, redactHarZip, resolveOpenApiOperation, resolveSecretStore, runRequest, runRequestForContract, runRequestForHar, runRequestToHar, runScript, runSequence, runSequenceForHar, runSequenceToHar, summarizeHar, synthesizeRedactedHarZip, validateCapturedTraffic, validateGraphqlOperation, validateOpenApiRequest, validateOpenApiResponse, validateSchema, writeImported };
|
|
898
|
+
//# sourceMappingURL=index.d.mts.map
|