@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.
@@ -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