@powerduck/openapi-request 0.2.2
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/README.md +711 -0
- package/dist/credentials-Cl0G4KpE.d.cts +783 -0
- package/dist/credentials-rqEKODvf.d.ts +783 -0
- package/dist/index-1jRrFc3d.d.cts +215 -0
- package/dist/index-CsRXyS7O.d.ts +215 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +350 -0
- package/dist/index.d.ts +350 -0
- package/dist/index.js +1 -0
- package/dist/protocol-D7sEx8IP.d.ts +62 -0
- package/dist/protocol-Dh-wN2nY.d.cts +62 -0
- package/dist/protocols/graphql/index.cjs +1 -0
- package/dist/protocols/graphql/index.d.cts +95 -0
- package/dist/protocols/graphql/index.d.ts +95 -0
- package/dist/protocols/graphql/index.js +1 -0
- package/dist/protocols/grpc/index.cjs +1 -0
- package/dist/protocols/grpc/index.d.cts +61 -0
- package/dist/protocols/grpc/index.d.ts +61 -0
- package/dist/protocols/grpc/index.js +1 -0
- package/dist/protocols/http/index.cjs +1 -0
- package/dist/protocols/http/index.d.cts +161 -0
- package/dist/protocols/http/index.d.ts +161 -0
- package/dist/protocols/http/index.js +1 -0
- package/dist/protocols/mcp/index.cjs +1 -0
- package/dist/protocols/mcp/index.d.cts +3 -0
- package/dist/protocols/mcp/index.d.ts +3 -0
- package/dist/protocols/mcp/index.js +1 -0
- package/dist/protocols/ws/index.cjs +1 -0
- package/dist/protocols/ws/index.d.cts +35 -0
- package/dist/protocols/ws/index.d.ts +35 -0
- package/dist/protocols/ws/index.js +1 -0
- package/dist/types-C9ifzKqk.d.cts +1226 -0
- package/dist/types-C9ifzKqk.d.ts +1226 -0
- package/package.json +93 -0
package/README.md
ADDED
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
# @powerduck/openapi-request
|
|
2
|
+
|
|
3
|
+
OpenAPI 3.2-first protocol debugger for HTTP, SSE, WebSocket, GraphQL, gRPC and MCP.
|
|
4
|
+
One client surface, one event model, one write-back pipeline.
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
|
|
8
|
+
- **OpenAPI 3.2** as the single source of truth — every protocol is an operation with extensions.
|
|
9
|
+
- **Incremental SSE / ndjson streaming** with `onEvent` callbacks and `onResponseStart` early classification.
|
|
10
|
+
- **Manual sessions** for WebSocket, gRPC (all four RPC modes) and MCP (Streamable HTTP + stdio).
|
|
11
|
+
- **gRPC** via reflection or `.proto` files, with metadata / status / trailers surfaced as events.
|
|
12
|
+
- **MCP** tools, prompts, resources and resource templates over HTTP or stdio.
|
|
13
|
+
- **GraphQL** introspection, query/mutation generation and subscription over WebSocket.
|
|
14
|
+
- **Postman-runtime** under the hood for HTTP, with full script (pre-request / test) support.
|
|
15
|
+
- **Response write-back** — merges live responses into the OpenAPI document as `responses` objects.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @powerduck/openapi-request
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Requires Node.js >= 18.17.
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { createClient } from "@powerduck/openapi-request";
|
|
29
|
+
|
|
30
|
+
const client = createClient();
|
|
31
|
+
|
|
32
|
+
// 1. Decide how to render before sending anything.
|
|
33
|
+
const prepared = client.prepare({
|
|
34
|
+
spec: openApiDoc,
|
|
35
|
+
target: { path: "/users/{id}", method: "get" },
|
|
36
|
+
});
|
|
37
|
+
console.log(prepared.display.mode); // "response" | "event-list" | "duplex-session"
|
|
38
|
+
console.log(prepared.stream.kind); // "none" | "sse" | "grpc-unary" | ...
|
|
39
|
+
|
|
40
|
+
// 2. Send a one-shot request (HTTP / GraphQL / MCP one-shot).
|
|
41
|
+
const result = await client.send({
|
|
42
|
+
spec: openApiDoc,
|
|
43
|
+
target: { path: "/users/{id}", method: "get" },
|
|
44
|
+
values: { path: { id: "42" } },
|
|
45
|
+
serverUrl: "https://api.example.com",
|
|
46
|
+
onResponseStart: (info) => {
|
|
47
|
+
console.log("streaming:", info.streaming); // true for SSE, switch UI early
|
|
48
|
+
},
|
|
49
|
+
onEvent: (event) => {
|
|
50
|
+
console.log(event.event, event.data); // incremental SSE events
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// 3. Open a long-lived session (WebSocket / gRPC / MCP).
|
|
55
|
+
const session = client.connect({ kind: "websocket", url: "wss://api.example.com/ws" });
|
|
56
|
+
await session.open();
|
|
57
|
+
session.onEvent((e) => console.log(e));
|
|
58
|
+
await session.send({ type: "ping" });
|
|
59
|
+
await session.close();
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## API Reference
|
|
65
|
+
|
|
66
|
+
### `createClient(options?)`
|
|
67
|
+
|
|
68
|
+
Returns a client with `prepare`, `send`, `sendMany`, `connect`, `discover`, `writeback`, `dispose`, `probeStreamingResponse`.
|
|
69
|
+
|
|
70
|
+
| Option | Type | Default | Description |
|
|
71
|
+
|--------|------|---------|-------------|
|
|
72
|
+
| `writeBack` | `WriteBackOptions` | `{}` | Controls how responses are merged into the spec. |
|
|
73
|
+
| `response` | `ToResponseOptions` | `{}` | Controls how a live response becomes an OpenAPI Response object. |
|
|
74
|
+
|
|
75
|
+
### `client.prepare(options)` → `PreparedRequest`
|
|
76
|
+
|
|
77
|
+
Pure, synchronous. No I/O. Returns everything the UI needs to choose a renderer.
|
|
78
|
+
|
|
79
|
+
| Field | Type | Description |
|
|
80
|
+
|-------|------|-------------|
|
|
81
|
+
| `protocol` | `string` | `"http"` \| `"sse"` \| `"websocket"` \| `"grpc"` \| `"graphql"` \| `"mcp"` |
|
|
82
|
+
| `transport` | `string` | `"http"` \| `"websocket"` \| `"grpc"` \| `"stdio"` |
|
|
83
|
+
| `display.mode` | `"response"` \| `"event-list"` \| `"duplex-session"` | How to render the result. |
|
|
84
|
+
| `stream.kind` | `StreamKind` | Fine-grained streaming taxonomy. |
|
|
85
|
+
| `stream.expected` | `boolean` | True when the operation declares or implies streaming. |
|
|
86
|
+
| `openapi.extensions` | `Record<string, unknown>` | The seven `x-*` extensions derived from the operation. |
|
|
87
|
+
| `warnings` | `string[]` | Non-fatal issues found during preparation. |
|
|
88
|
+
|
|
89
|
+
### `client.send(options)` → `Promise<SendResult>`
|
|
90
|
+
|
|
91
|
+
One-shot execution through the full pipeline: build collection → run → parse → write back.
|
|
92
|
+
|
|
93
|
+
### `client.connect(options)` → `ManualSession`
|
|
94
|
+
|
|
95
|
+
Opens a long-lived session for WebSocket, gRPC or MCP. See [Manual Sessions](#manual-sessions).
|
|
96
|
+
|
|
97
|
+
### `client.discover(options)` → `Promise<DiscoveryResult>`
|
|
98
|
+
|
|
99
|
+
Discovers schema/capabilities for MCP or gRPC.
|
|
100
|
+
|
|
101
|
+
| Protocol | Options |
|
|
102
|
+
|----------|---------|
|
|
103
|
+
| `mcp` | `{ protocol: "mcp", endpoint, headers?, transport?, command?, args?, cwd? }` |
|
|
104
|
+
| `grpc` | `{ protocol: "grpc", address, reflection?, protoPaths?, includeDirs?, metadata?, channelOptions? }` |
|
|
105
|
+
|
|
106
|
+
### `client.writeback(spec, prepared, result, options?)` → `OpenApiDocument`
|
|
107
|
+
|
|
108
|
+
Merges a result's response into the spec at the operation's path+method.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## SendOptions (all protocols)
|
|
113
|
+
|
|
114
|
+
The full option shape passed to `client.send()`. Protocol-specific fields are nested under `runner`, `websocket`, `graphql`, `mcp`, `grpc`.
|
|
115
|
+
|
|
116
|
+
| Field | Type | Default | Description |
|
|
117
|
+
|-------|------|---------|-------------|
|
|
118
|
+
| `spec` | `OpenApiDocument` | **required** | The OpenAPI 3.2 document. |
|
|
119
|
+
| `target` | `OperationTarget` | **required** | `{ path?, method?, operationId? }` — identifies the operation. |
|
|
120
|
+
| `values` | `RequestValues` | `{}` | Path/query/header/cookie/body values to inject. |
|
|
121
|
+
| `serverUrl` | `string` | `spec.servers[0].url` | Overrides the server URL. |
|
|
122
|
+
| `serverVariables` | `Record<string,string>` | `{}` | Variables for `{var}` placeholders in the server URL. |
|
|
123
|
+
| `variables` | `Record<string,string>` | `{}` | Postman environment variables (`{{name}}`). |
|
|
124
|
+
| `globals` | `Record<string,string>` | `{}` | Postman global variables. |
|
|
125
|
+
| `localVariables` | `Record<string,string>` | `{}` | Postman local variables. |
|
|
126
|
+
| `auth` | `AuthConfig` | — | `{ type: "bearer"\|"basic"\|"apikey"\|"none", ... }`. |
|
|
127
|
+
| `scripts` | `ScriptConfig` | `{}` | Pre-request / test scripts and `x-postman-scripts` handling. |
|
|
128
|
+
| `runner` | `RuntimeRunOptions` | `{}` | Full postman-runtime passthrough. Highest precedence. |
|
|
129
|
+
| `websocket` | `WebSocketOptions` | `{}` | WebSocket-specific options. |
|
|
130
|
+
| `graphql` | `GraphQLOptions` | `{}` | GraphQL-specific options. |
|
|
131
|
+
| `mcp` | `McpOptions` | `{}` | MCP-specific options. |
|
|
132
|
+
| `grpc` | `any` | `{}` | gRPC-specific options (used by the gRPC adapter). |
|
|
133
|
+
| `timeout` | `number` | `30000` | Convenience shortcut for `runner.timeout.request`. `0` = unlimited. |
|
|
134
|
+
| `maxEvents` | `number` | `100` | Maximum streaming events to retain. |
|
|
135
|
+
| `maxStreamMs` | `number` | `30000` | Maximum streaming duration before sampling stops. |
|
|
136
|
+
| `maxResponseSize` | `number` | — | Hard byte ceiling for the response body. Rejected if `<= 0`. |
|
|
137
|
+
| `writeBack` | `boolean` | `true` | Set `false` to skip OpenAPI write-back. |
|
|
138
|
+
| `signal` | `AbortSignal` | — | Cancels the run; returns a partial result. |
|
|
139
|
+
| `onEvent` | `(event: StreamEvent) => void` | — | Incremental SSE / ndjson events. Called defensively (errors swallowed). |
|
|
140
|
+
| `onResponseStart` | `(info: ResponseStartInfo) => void` | — | Fires when headers arrive. `info.streaming` is the early SSE flag. |
|
|
141
|
+
| `onConsole` | `(log: ConsoleLog) => void` | — | Script `console.*` output. |
|
|
142
|
+
| `onAssertion` | `(a: AssertionResult) => void` | — | Individual test assertion results. |
|
|
143
|
+
| `onOpen` | `(info: {url, protocol?, headers}) => void` | — | WebSocket connection established. |
|
|
144
|
+
|
|
145
|
+
### StreamParserOptions (also on SendOptions)
|
|
146
|
+
|
|
147
|
+
| Field | Type | Default | Description |
|
|
148
|
+
|-------|------|---------|-------------|
|
|
149
|
+
| `maxBufferChars` | `number` | `4194304` (4 Mi) | Max chars buffered while waiting for an event boundary. |
|
|
150
|
+
| `maxEventChars` | `number` | `1048576` (1 Mi) | Max chars in a single event's `data`. |
|
|
151
|
+
| `inheritEventId` | `boolean` | `true` | Attach the last seen `id` to events that omit one. Set `false` for spec-faithful streams. |
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## HTTP / SSE
|
|
156
|
+
|
|
157
|
+
### Request JSON structure
|
|
158
|
+
|
|
159
|
+
An HTTP operation is a standard OpenAPI path item. SSE is detected automatically from the response content type or the `x-response-stream` extension.
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"openapi": "3.2.0",
|
|
164
|
+
"info": { "title": "example", "version": "1.0.0" },
|
|
165
|
+
"servers": [{ "url": "https://api.example.com" }],
|
|
166
|
+
"paths": {
|
|
167
|
+
"/users/{id}": {
|
|
168
|
+
"get": {
|
|
169
|
+
"operationId": "getUser",
|
|
170
|
+
"parameters": [
|
|
171
|
+
{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
|
|
172
|
+
],
|
|
173
|
+
"responses": {
|
|
174
|
+
"200": {
|
|
175
|
+
"description": "ok",
|
|
176
|
+
"content": { "application/json": {} }
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
"/events": {
|
|
182
|
+
"get": {
|
|
183
|
+
"operationId": "streamEvents",
|
|
184
|
+
"responses": {
|
|
185
|
+
"200": {
|
|
186
|
+
"description": "ok",
|
|
187
|
+
"content": { "text/event-stream": {} }
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### `runner.requester` options (HTTP transport)
|
|
197
|
+
|
|
198
|
+
These control the underlying HTTP client. Pass via `send({ runner: { requester: { ... } } })`.
|
|
199
|
+
|
|
200
|
+
| Field | Type | Default | Description |
|
|
201
|
+
|-------|------|---------|-------------|
|
|
202
|
+
| `followRedirects` | `boolean` | `true` | Follow HTTP 3xx redirects. |
|
|
203
|
+
| `followOriginalHttpMethod` | `boolean` | `false` | Keep the original method on redirect (e.g. POST stays POST). When `false`, 301/302/303 downgrade to GET. |
|
|
204
|
+
| `maxRedirects` | `number` | `10` | Maximum redirect hops. Must be a non-negative integer. |
|
|
205
|
+
| `protocolVersion` | `"http1" \| "http2" \| "auto"` | `"http1"` | HTTP protocol version. `"http1"` is required for SSE chunked streaming; use `"auto"` for HTTP/2 endpoints. |
|
|
206
|
+
| `strictSSL` | `boolean` | `true` | Reject self-signed / invalid TLS certificates. |
|
|
207
|
+
| `insecureHTTPParser` | `boolean` | `false` | Allow invalid HTTP responses (e.g. malformed headers). |
|
|
208
|
+
| `maxResponseSize` | `number` | — | Hard byte ceiling for the response body. `0` is rejected (would silently yield empty streams). Omit for unbounded. |
|
|
209
|
+
| `maxHeaderSize` | `number` | — | Maximum response header size in bytes. |
|
|
210
|
+
| `useWhatWGUrlParser` | `boolean` | `true` | Use the WHATWG URL parser for request URLs. |
|
|
211
|
+
| `removeRefererHeaderOnRedirect` | `boolean` | `false` | Strip the `Referer` header when following a redirect. |
|
|
212
|
+
| `timings` | `boolean` | `true` | Collect detailed timing data (powers `firstByteMs`, `networkDurationMs`). |
|
|
213
|
+
| `verbose` | `boolean` | `true` | Keep request/response history (powers `replays`). |
|
|
214
|
+
| `implicitCacheControl` | `boolean` | `true` | Add `Cache-Control: no-cache` implicitly. |
|
|
215
|
+
| `implicitTraceHeader` | `boolean` | `true` | Add a trace header implicitly. |
|
|
216
|
+
| `disableCookies` | `boolean` | `false` | Disable the cookie jar. |
|
|
217
|
+
| `cookieJar` | `any` | — | Custom cookie jar instance. |
|
|
218
|
+
| `systemHeaders` | `Record<string,string>` | — | Headers added to every request (e.g. `User-Agent`). |
|
|
219
|
+
| `extendedRootCA` | `string` | — | Path to an additional CA bundle. |
|
|
220
|
+
| `encoding` | `null \| string` | `null` | Response body encoding. **Must be `null` for incremental SSE streaming** — the default `utf8` buffers the body to decode multi-byte boundaries. |
|
|
221
|
+
| `agents` | `{ http?, https? }` | — | Custom HTTP/HTTPS agents. Supplying agents disables the library's socket tracking (stream cancellation falls back to `run.abort()`). |
|
|
222
|
+
| `network.hostLookup` | `{ type, hostIpMap? }` | — | Custom DNS resolution. |
|
|
223
|
+
| `network.restrictedAddresses` | `Record<string,boolean>` | — | Block specific IP addresses (SSRF protection). |
|
|
224
|
+
| `maxInvokableNestedRequests` | `number` | `5` | Max nested requests from scripts (`pm.sendRequest`). |
|
|
225
|
+
| `sslKeyLogFile` | `string` | — | Path to write TLS session keys (for Wireshark debugging). |
|
|
226
|
+
|
|
227
|
+
### `runner` top-level options
|
|
228
|
+
|
|
229
|
+
| Field | Type | Default | Description |
|
|
230
|
+
|-------|------|---------|-------------|
|
|
231
|
+
| `timeout.request` | `number` | `30000` | Per-request timeout in ms. `0` = unlimited. |
|
|
232
|
+
| `timeout.script` | `number` | `15000` | Script execution timeout in ms. |
|
|
233
|
+
| `timeout.global` | `number` | computed | Global run timeout. Derived from `request + streamBudget + 15000` slack. |
|
|
234
|
+
| `iterationCount` | `number` | `1` | Number of iterations. Must be >= 1. |
|
|
235
|
+
| `data` | `Array<Record<string,unknown>>` | — | Data-driven iteration values. When provided, `iterationCount` defaults to `data.length`. |
|
|
236
|
+
| `stopOnError` | `boolean` | `false` | Halt the run on a request error. |
|
|
237
|
+
| `abortOnError` | `boolean` | `false` | Abort immediately on error (no cleanup callbacks). |
|
|
238
|
+
| `stopOnFailure` | `boolean` | `false` | Halt on a failed test assertion. |
|
|
239
|
+
| `abortOnFailure` | `boolean` | `false` | Abort immediately on assertion failure. |
|
|
240
|
+
| `environment` | `any` | built from `variables` | Postman environment. Override to supply a custom VariableScope. |
|
|
241
|
+
| `globals` | `any` | built from `globals` | Postman globals. |
|
|
242
|
+
| `localVariables` | `any` | built from `localVariables` | Postman local variables. |
|
|
243
|
+
| `proxies` | `any` | — | Postman proxy configuration list. |
|
|
244
|
+
| `systemProxy` | `(url, cb) => void` | — | Callback to resolve the system proxy for a URL. |
|
|
245
|
+
| `ignoreProxyEnvironmentVariables` | `boolean` | `false` | Ignore `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` env vars. |
|
|
246
|
+
| `certificates` | `any` | — | Client certificate list. |
|
|
247
|
+
| `delay.item` | `number` | `0` | Delay between requests in ms. |
|
|
248
|
+
| `delay.iteration` | `number` | `0` | Delay between iterations in ms. |
|
|
249
|
+
| `entrypoint.execute` | `string` | — | Name of the item to start from. |
|
|
250
|
+
| `entrypoint.lookupStrategy` | `"idOrName" \| "path"` | — | How to resolve the entrypoint. |
|
|
251
|
+
| `secretResolver` | `(ctx, cb) => void` | — | Resolve secrets referenced in the collection. |
|
|
252
|
+
| `fileResolver` | `unknown` | — | Resolve file references in request bodies. |
|
|
253
|
+
|
|
254
|
+
### SSE streaming example
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
const result = await client.send({
|
|
258
|
+
spec,
|
|
259
|
+
target: { path: "/events", method: "get" },
|
|
260
|
+
maxStreamMs: 60000,
|
|
261
|
+
maxEvents: 100,
|
|
262
|
+
onResponseStart: (info) => {
|
|
263
|
+
if (info.streaming) {
|
|
264
|
+
// Switch the UI to the event-list view immediately — do not wait
|
|
265
|
+
// for send() to resolve.
|
|
266
|
+
renderEventList();
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
onEvent: (event) => {
|
|
270
|
+
appendEvent(event); // called incrementally, ~every chunk
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// result.response.events contains all retained events.
|
|
275
|
+
// result.response.truncated / stopReason explain why sampling ended.
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## WebSocket
|
|
281
|
+
|
|
282
|
+
### Request JSON structure
|
|
283
|
+
|
|
284
|
+
```json
|
|
285
|
+
{
|
|
286
|
+
"paths": {
|
|
287
|
+
"/ws": {
|
|
288
|
+
"get": {
|
|
289
|
+
"operationId": "wsConnect",
|
|
290
|
+
"x-protocol": "websocket",
|
|
291
|
+
"x-ws": {
|
|
292
|
+
"url": "wss://api.example.com/ws",
|
|
293
|
+
"subprotocols": ["graphql-transport-ws"]
|
|
294
|
+
},
|
|
295
|
+
"responses": { "200": { "description": "duplex" } }
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### `websocket` options
|
|
303
|
+
|
|
304
|
+
| Field | Type | Default | Description |
|
|
305
|
+
|-------|------|---------|-------------|
|
|
306
|
+
| `url` | `string` | from `x-ws.url` | Absolute `ws://` or `wss://` URL. Overrides the spec. |
|
|
307
|
+
| `subprotocols` | `string[]` | `[]` | WebSocket subprotocols to negotiate. |
|
|
308
|
+
| `headers` | `Record<string,string>` | `{}` | Extra handshake headers. |
|
|
309
|
+
| `send` | `Array<string \| object \| Uint8Array>` | — | Messages sent immediately after open. |
|
|
310
|
+
| `sendDelayMs` | `number` | — | Delay between consecutive `send` messages. |
|
|
311
|
+
| `maxMessages` | `number` | — | Stop after this many inbound messages. |
|
|
312
|
+
| `maxSessionMs` | `number` | — | Hard cap on total session duration. |
|
|
313
|
+
| `idleTimeoutMs` | `number` | — | Close when no message arrives within this window. |
|
|
314
|
+
| `keepAlive` | `{ intervalMs, payload? }` | — | Application-level ping on an interval. |
|
|
315
|
+
| `closeCode` | `number` | `1000` | Close code sent when terminating. |
|
|
316
|
+
| `closeReason` | `string` | `"Closed"` | Close reason text. |
|
|
317
|
+
| `closeTimeoutMs` | `number` | — | Grace period for the peer's close frame before destroying the socket. |
|
|
318
|
+
| `rejectUnauthorized` | `boolean` | `true` | Reject self-signed TLS certificates. |
|
|
319
|
+
| `maxPayloadBytes` | `number` | — | Cap on retained payload size per frame. |
|
|
320
|
+
| `handshakeTimeoutMs` | `number` | `15000` | Handshake timeout. |
|
|
321
|
+
| `clientOptions` | `Record<string,unknown>` | `{}` | Extra options forwarded to the `ws` constructor. |
|
|
322
|
+
|
|
323
|
+
### Manual WebSocket session
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
const session = client.connect({
|
|
327
|
+
kind: "websocket",
|
|
328
|
+
url: "wss://api.example.com/ws",
|
|
329
|
+
subprotocols: ["chat"],
|
|
330
|
+
headers: { Authorization: "Bearer xxx" },
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
session.onEvent((e) => {
|
|
334
|
+
// e.kind: "open" | "text" | "binary" | "error" | "close" | "upgrade"
|
|
335
|
+
// e.direction: "in" | "out" | "meta"
|
|
336
|
+
// e.data: text payload (or base64 for binary)
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
await session.open();
|
|
340
|
+
await session.send({ type: "subscribe", channel: "updates" });
|
|
341
|
+
await session.close({ code: 1000, reason: "done" });
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## GraphQL
|
|
347
|
+
|
|
348
|
+
### Request JSON structure
|
|
349
|
+
|
|
350
|
+
```json
|
|
351
|
+
{
|
|
352
|
+
"paths": {
|
|
353
|
+
"/graphql": {
|
|
354
|
+
"post": {
|
|
355
|
+
"operationId": "graphqlQuery",
|
|
356
|
+
"x-protocol": "graphql",
|
|
357
|
+
"x-graphql": {
|
|
358
|
+
"endpoint": "https://api.example.com/graphql",
|
|
359
|
+
"operationType": "query",
|
|
360
|
+
"operationName": "GetUser",
|
|
361
|
+
"query": "query GetUser($id: ID!) { user(id: $id) { name } }",
|
|
362
|
+
"variablesSchema": {
|
|
363
|
+
"type": "object",
|
|
364
|
+
"properties": { "id": { "type": "string" } },
|
|
365
|
+
"required": ["id"]
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
"requestBody": {
|
|
369
|
+
"content": { "application/json": { "schema": { "type": "object" } } }
|
|
370
|
+
},
|
|
371
|
+
"responses": { "200": { "description": "ok" } }
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### `graphql` options
|
|
379
|
+
|
|
380
|
+
| Field | Type | Default | Description |
|
|
381
|
+
|-------|------|---------|-------------|
|
|
382
|
+
| `endpoint` | `string` | from `x-graphql.endpoint` | Absolute HTTP(S) URL of the GraphQL endpoint. |
|
|
383
|
+
| `query` | `string` | from `x-graphql.query` | Query or mutation document. Overrides the spec. |
|
|
384
|
+
| `operationName` | `string` | from `x-graphql.operationName` | Required when `query` declares more than one operation. |
|
|
385
|
+
| `variables` | `Record<string,unknown>` | `{}` | GraphQL variables. Merged over sampled values. |
|
|
386
|
+
| `headers` | `Record<string,string>` | `{}` | Extra headers, merged over `values.header` and auth. |
|
|
387
|
+
| `useGet` | `boolean` | `false` | Use HTTP GET with querystring-encoded `query`/`variables` instead of POST. |
|
|
388
|
+
|
|
389
|
+
### GraphQL discovery (introspection)
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
const result = await client.discover({
|
|
393
|
+
protocol: "graphql",
|
|
394
|
+
endpoint: "https://countries.trevorblades.com/",
|
|
395
|
+
headers: { Authorization: "Bearer xxx" },
|
|
396
|
+
});
|
|
397
|
+
// result.discovery contains the introspected schema
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
## gRPC
|
|
403
|
+
|
|
404
|
+
### Request JSON structure
|
|
405
|
+
|
|
406
|
+
```json
|
|
407
|
+
{
|
|
408
|
+
"paths": {
|
|
409
|
+
"/grpc": {
|
|
410
|
+
"post": {
|
|
411
|
+
"operationId": "grpcCall",
|
|
412
|
+
"x-protocol": "grpc",
|
|
413
|
+
"x-grpc": {
|
|
414
|
+
"address": "127.0.0.1:50051",
|
|
415
|
+
"service": "demo.echo.Echo",
|
|
416
|
+
"method": "Say",
|
|
417
|
+
"kind": "unary",
|
|
418
|
+
"reflection": true
|
|
419
|
+
},
|
|
420
|
+
"responses": { "200": { "description": "ok" } }
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### gRPC method kinds
|
|
428
|
+
|
|
429
|
+
| Kind | Streaming | Interaction | Button label |
|
|
430
|
+
|------|-----------|-------------|-------------|
|
|
431
|
+
| `unary` | none | One request → one response. Session closes automatically. | "Send and await" |
|
|
432
|
+
| `server_streaming` | server → client | One request → N responses. Session closes on stream end. | "Send and await" |
|
|
433
|
+
| `client_streaming` | client → server | N requests → one response. Send messages, then Finish. | "Send message" + "Finish" |
|
|
434
|
+
| `bidi_streaming` | both | N requests ↔ N responses. Full duplex. | "Send message" + "Finish" |
|
|
435
|
+
|
|
436
|
+
### Manual gRPC session options
|
|
437
|
+
|
|
438
|
+
| Field | Type | Default | Description |
|
|
439
|
+
|-------|------|---------|-------------|
|
|
440
|
+
| `address` | `string` | **required** | `host:port`, no scheme. |
|
|
441
|
+
| `service` | `string` | **required** | Fully-qualified service name, e.g. `"demo.echo.Echo"`. |
|
|
442
|
+
| `method` | `string` | **required** | Method name as declared in proto. Matched case-insensitively as fallback. |
|
|
443
|
+
| `reflection` | `boolean` | — | Set `true` to use server reflection. Mutually exclusive with `protoPaths`. |
|
|
444
|
+
| `protoPaths` | `string[]` | — | `.proto` files or directories. Directories are walked recursively. |
|
|
445
|
+
| `includeDirs` | `string[]` | derived | Import roots for proto-loader. |
|
|
446
|
+
| `metadata` | `Record<string, string \| string[]>` | `{}` | gRPC metadata sent on every call. |
|
|
447
|
+
| `deadlineMs` | `number` | — | Per-call deadline. Enforced by the server (yields DEADLINE_EXCEEDED). |
|
|
448
|
+
| `channelOptions` | `Record<string,unknown>` | `{}` | grpc-js channel options. |
|
|
449
|
+
| `tls` | `boolean \| GrpcTlsOptions` | — | `false`/undefined = insecure; `true` = TLS with system roots; object = custom TLS. |
|
|
450
|
+
| `loaderOptions` | `Record<string,unknown>` | `{}` | Extra proto-loader options. |
|
|
451
|
+
| `reflectionTimeoutMs` | `number` | `5000` | Budget for the whole reflection session. |
|
|
452
|
+
| `reflectionVersion` | `"v1" \| "v1alpha"` | try v1, fall back | Pin a reflection version. |
|
|
453
|
+
| `reflectionHost` | `string` | — | `host` field on reflection requests (virtual-hosted servers). |
|
|
454
|
+
|
|
455
|
+
### gRPC TLS options
|
|
456
|
+
|
|
457
|
+
| Field | Type | Description |
|
|
458
|
+
|-------|------|-------------|
|
|
459
|
+
| `rootCerts` | `Buffer` | CA bundle contents (not a path). Pass `await readFile(p)`. |
|
|
460
|
+
| `privateKey` | `Buffer` | Client key for mTLS. Must be given with `certChain`. |
|
|
461
|
+
| `certChain` | `Buffer` | Client certificate chain for mTLS. Must be given with `privateKey`. |
|
|
462
|
+
| `skipHostnameVerification` | `boolean` | Skip hostname verification only. Chain is still verified. Always produces a warning. |
|
|
463
|
+
|
|
464
|
+
### gRPC session events
|
|
465
|
+
|
|
466
|
+
| `kind` | `direction` | Payload |
|
|
467
|
+
|--------|-------------|---------|
|
|
468
|
+
| `open` | `meta` | `{ address, service, method, kind, descriptorSource }` |
|
|
469
|
+
| `metadata` | `meta` | Response headers (initial metadata). |
|
|
470
|
+
| `data` | `out` | Outbound request message. |
|
|
471
|
+
| `data` | `in` | Inbound response message. |
|
|
472
|
+
| `status` | `meta` | Terminal status: `{ code, statusName, details, metadata }` (trailers). |
|
|
473
|
+
| `error` | `meta` | Error: `{ code, statusName, details, error }`. |
|
|
474
|
+
| `end` | `meta` | Stream ended. |
|
|
475
|
+
| `close` | `meta` | Session closed by the client. |
|
|
476
|
+
|
|
477
|
+
### gRPC example
|
|
478
|
+
|
|
479
|
+
```ts
|
|
480
|
+
const session = client.connect({
|
|
481
|
+
kind: "grpc",
|
|
482
|
+
address: "127.0.0.1:50051",
|
|
483
|
+
reflection: true,
|
|
484
|
+
service: "demo.echo.Echo",
|
|
485
|
+
method: "Say",
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
session.onEvent((e) => {
|
|
489
|
+
if (e.kind === "status") {
|
|
490
|
+
console.log("status:", e.meta.code, e.meta.statusName);
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
await session.open();
|
|
495
|
+
await session.send({ text: "hello" });
|
|
496
|
+
// session.state === "closed" after a unary call
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
501
|
+
## MCP (Model Context Protocol)
|
|
502
|
+
|
|
503
|
+
### Request JSON structure
|
|
504
|
+
|
|
505
|
+
```json
|
|
506
|
+
{
|
|
507
|
+
"paths": {
|
|
508
|
+
"/mcp": {
|
|
509
|
+
"post": {
|
|
510
|
+
"operationId": "mcpCall",
|
|
511
|
+
"x-protocol": "mcp",
|
|
512
|
+
"x-transport": "http",
|
|
513
|
+
"x-mcp": {
|
|
514
|
+
"endpoint": "http://127.0.0.1:4200/mcp",
|
|
515
|
+
"method": "tools/call",
|
|
516
|
+
"name": "get_weather",
|
|
517
|
+
"arguments": { "city": "SF" }
|
|
518
|
+
},
|
|
519
|
+
"responses": { "200": { "description": "ok" } }
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### `mcp` options
|
|
527
|
+
|
|
528
|
+
| Field | Type | Default | Description |
|
|
529
|
+
|-------|------|---------|-------------|
|
|
530
|
+
| `transport` | `"streamable-http" \| "stdio"` | `"streamable-http"` | Wire transport. |
|
|
531
|
+
| `endpoint` | `string` | from `x-mcp.endpoint` | Absolute http(s) URL for Streamable HTTP transport. |
|
|
532
|
+
| `headers` | `Record<string,string>` | `{}` | Extra HTTP headers. |
|
|
533
|
+
| `command` | `string` | — | stdio command (e.g. `"npx"`). Required for stdio. |
|
|
534
|
+
| `args` | `string[]` | `[]` | stdio command arguments. |
|
|
535
|
+
| `cwd` | `string` | — | Working directory for the stdio child. |
|
|
536
|
+
| `env` | `Record<string,string \| undefined>` | — | Environment for the stdio child. |
|
|
537
|
+
| `timeoutMs` | `number` | `30000` | Per-request timeout. `0` disables. |
|
|
538
|
+
| `maxBufferBytes` | `number` | — | stdio stdout buffer cap. |
|
|
539
|
+
| `maxStderrBytes` | `number` | — | stdio stderr cap before the child is killed. |
|
|
540
|
+
| `method` | `string` | from `x-mcp.method` | JSON-RPC method, e.g. `"tools/call"`. |
|
|
541
|
+
| `name` | `string` | from `x-mcp.name` | Target tool/prompt/resource name. |
|
|
542
|
+
| `arguments` | `Record<string,unknown>` | `{}` | JSON-RPC params. |
|
|
543
|
+
| `sessionId` | `string` | — | Reuse an existing MCP session ID. |
|
|
544
|
+
| `protocolVersion` | `string` | — | MCP protocol version to negotiate. |
|
|
545
|
+
| `clientInfo` | `{ name, version }` | `{ name: "powerduck", version: "0.1.0" }` | Client info sent in `initialize`. |
|
|
546
|
+
|
|
547
|
+
### Manual MCP session
|
|
548
|
+
|
|
549
|
+
```ts
|
|
550
|
+
const session = client.connect({
|
|
551
|
+
kind: "mcp",
|
|
552
|
+
transport: "streamable-http",
|
|
553
|
+
endpoint: "http://127.0.0.1:4200/mcp",
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
await session.open();
|
|
557
|
+
|
|
558
|
+
// List capabilities
|
|
559
|
+
const tools = await session.listTools();
|
|
560
|
+
const prompts = await session.listPrompts();
|
|
561
|
+
const resources = await session.listResources();
|
|
562
|
+
|
|
563
|
+
// Call a tool
|
|
564
|
+
const result = await session.callTool("get_weather", { city: "SF" });
|
|
565
|
+
|
|
566
|
+
// Raw JSON-RPC
|
|
567
|
+
const raw = await session.request("tools/call", { name: "x", arguments: {} });
|
|
568
|
+
|
|
569
|
+
await session.close();
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
### MCP stdio session
|
|
573
|
+
|
|
574
|
+
```ts
|
|
575
|
+
const session = client.connect({
|
|
576
|
+
kind: "mcp",
|
|
577
|
+
transport: "stdio",
|
|
578
|
+
command: "npx",
|
|
579
|
+
args: ["-y", "@modelcontextprotocol/server-everything"],
|
|
580
|
+
cwd: "/path/to/project",
|
|
581
|
+
});
|
|
582
|
+
await session.open();
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
---
|
|
586
|
+
|
|
587
|
+
## Manual Sessions (unified API)
|
|
588
|
+
|
|
589
|
+
All manual sessions (WebSocket, gRPC, MCP) share this contract:
|
|
590
|
+
|
|
591
|
+
| Method / Property | Description |
|
|
592
|
+
|-------------------|-------------|
|
|
593
|
+
| `protocol` | `"websocket"` \| `"grpc"` \| `"mcp"` |
|
|
594
|
+
| `state` | `"idle"` \| `"connecting"` \| `"open"` \| `"closing"` \| `"closed"` \| `"error"` |
|
|
595
|
+
| `events` | Read-only array of all events (ring-buffered at 1000 by default). |
|
|
596
|
+
| `onEvent(listener)` | Subscribe to events. Returns `{ unsubscribe() }`. |
|
|
597
|
+
| `open()` | Connect / initialize. Rejects from non-idle states. |
|
|
598
|
+
| `send(message, options?)` | Send a message. Rejects when not open. |
|
|
599
|
+
| `close(options?)` | Close gracefully. Idempotent. |
|
|
600
|
+
| `waitForClose()` | Promise that resolves when the session closes. |
|
|
601
|
+
|
|
602
|
+
### SessionEventDTO
|
|
603
|
+
|
|
604
|
+
| Field | Type | Description |
|
|
605
|
+
|-------|------|-------------|
|
|
606
|
+
| `protocol` | `string` | Protocol name. |
|
|
607
|
+
| `transport` | `string` | Transport label. |
|
|
608
|
+
| `sessionId` | `string` | Unique session identifier. |
|
|
609
|
+
| `direction` | `"in" \| "out" \| "meta"` | Message direction. |
|
|
610
|
+
| `kind` | `string` | Event discriminator (protocol-specific). |
|
|
611
|
+
| `at` | `number` | Epoch ms timestamp. |
|
|
612
|
+
| `state` | `SessionState?` | Session state at the time of the event. |
|
|
613
|
+
| `data` | `Cloneable?` | Message payload. |
|
|
614
|
+
| `error` | `Cloneable?` | Error payload. |
|
|
615
|
+
| `meta` | `Cloneable?` | Metadata (headers, status code, etc.). |
|
|
616
|
+
|
|
617
|
+
---
|
|
618
|
+
|
|
619
|
+
## SendResult
|
|
620
|
+
|
|
621
|
+
| Field | Type | Description |
|
|
622
|
+
|-------|------|-------------|
|
|
623
|
+
| `protocol` | `ProtocolName` | `"http"` \| `"sse"` \| etc. |
|
|
624
|
+
| `request` | `{ method, url, headers, body? }` | The request that was sent. |
|
|
625
|
+
| `response.status` | `number` | HTTP status code. `0` if no response. |
|
|
626
|
+
| `response.statusText` | `string` | Status text. |
|
|
627
|
+
| `response.headers` | `Record<string,string>` | Response headers (case preserved). |
|
|
628
|
+
| `response.contentType` | `string?` | Parsed content type. |
|
|
629
|
+
| `response.body` | `unknown` | Parsed body (non-streaming). |
|
|
630
|
+
| `response.text` | `string?` | Raw body text. |
|
|
631
|
+
| `response.events` | `StreamEvent[]` | Collected streaming events. |
|
|
632
|
+
| `response.timings` | `{ startedAt, endedAt, durationMs, firstByteMs?, networkDurationMs? }` | Timing data. |
|
|
633
|
+
| `response.sizeBytes` | `number` | Body bytes received. |
|
|
634
|
+
| `response.truncated` | `boolean?` | True if sampling stopped before natural completion. |
|
|
635
|
+
| `response.stopReason` | `StopReason?` | Which limit fired: `"maxEvents" \| "maxStreamMs" \| "maxResponseSize" \| "aborted" \| "hardTimeout"`. |
|
|
636
|
+
| `response.droppedEvents` | `number?` | Events discarded by the parser's size caps. |
|
|
637
|
+
| `scripts` | `ScriptReport?` | Pre-request / test script results. |
|
|
638
|
+
| `cookies` | `Array<{name, value, domain?, path?}>` | Cookies set by the response. |
|
|
639
|
+
| `replays` | `ReplayRecord[]` | Auxiliary traffic (OAuth refreshes, redirects). |
|
|
640
|
+
| `error` | `{ message, code?, name?, stack? }?` | Fatal error, if any. |
|
|
641
|
+
| `collection` | `any` | Generated Postman collection (HTTP only). |
|
|
642
|
+
| `environment` | `any` | Generated Postman environment. |
|
|
643
|
+
| `responseFragment` | `any` | OpenAPI Response object derived from the live call. |
|
|
644
|
+
| `responseStatusCode` | `string` | Status code the fragment was filed under. |
|
|
645
|
+
| `patchedSpec` | `OpenApiDocument?` | Spec with the response merged in. |
|
|
646
|
+
| `writeBackSkippedReason` | `string?` | Why write-back was skipped. |
|
|
647
|
+
|
|
648
|
+
---
|
|
649
|
+
|
|
650
|
+
## Error Handling
|
|
651
|
+
|
|
652
|
+
All errors are thrown as `ProtoKitError` with a machine-readable `code`:
|
|
653
|
+
|
|
654
|
+
| Code | Meaning |
|
|
655
|
+
|------|---------|
|
|
656
|
+
| `BAD_OPTIONS` | Missing or invalid send options. |
|
|
657
|
+
| `BAD_RUN_OPTIONS` | Invalid runtime options (e.g. `maxResponseSize: 0`). |
|
|
658
|
+
| `BAD_COLLECTION` | Failed to construct the Postman collection. |
|
|
659
|
+
| `RUNTIME_INIT` | postman-runtime failed to initialize. |
|
|
660
|
+
| `RUNTIME_RUN` | The run failed with a done error. |
|
|
661
|
+
| `EXECUTION_FAILED` | A protocol adapter threw. |
|
|
662
|
+
| `BAD_MCP_ENDPOINT` | Invalid MCP endpoint URL. |
|
|
663
|
+
| `BAD_MCP_STDIO_COMMAND` | Missing stdio command. |
|
|
664
|
+
| `MCP_TIMEOUT` | MCP request or initialize timed out. |
|
|
665
|
+
| `MCP_NOT_OPEN` | Called before `open()`. |
|
|
666
|
+
| `MCP_SESSION_CLOSED` | Called on a closed session. |
|
|
667
|
+
| `MCP_SESSION_EXPIRED` | Server returned 404 for the session. |
|
|
668
|
+
| `MCP_RPC_ERROR` | JSON-RPC error response. |
|
|
669
|
+
| `MCP_EMPTY_RESPONSE` | No JSON-RPC message in the response. |
|
|
670
|
+
|
|
671
|
+
```ts
|
|
672
|
+
import { ProtoKitError } from "@powerduck/openapi-request";
|
|
673
|
+
|
|
674
|
+
try {
|
|
675
|
+
await client.send(options);
|
|
676
|
+
} catch (err) {
|
|
677
|
+
if (err instanceof ProtoKitError) {
|
|
678
|
+
console.error(err.code, err.message);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
---
|
|
684
|
+
|
|
685
|
+
## Development
|
|
686
|
+
|
|
687
|
+
```bash
|
|
688
|
+
npm install
|
|
689
|
+
npm run build # tsup → dist/
|
|
690
|
+
npm test # vitest run (107 tests)
|
|
691
|
+
npm run typecheck # tsc --noEmit
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
### Test layout
|
|
695
|
+
|
|
696
|
+
| File | Coverage |
|
|
697
|
+
|------|----------|
|
|
698
|
+
| `tests/sse-parser.test.ts` | 29 tests — SSE parser edge cases, size caps, chunk boundaries. |
|
|
699
|
+
| `tests/runner-options.test.ts` | 19 tests — HTTP option validation and defaults. |
|
|
700
|
+
| `tests/session-hub.test.ts` | 9 tests — event hub, listener safety, ring buffer. |
|
|
701
|
+
| `tests/grpc-session.test.ts` | 10 tests — unary success/error state, streaming, lifecycle. |
|
|
702
|
+
| `tests/ws-session.test.ts` | 10 tests — connect, send/receive, error state, lifecycle. |
|
|
703
|
+
| `tests/client.test.ts` | 8 tests — prepare() protocol inference. |
|
|
704
|
+
| `tests/session.test.ts` | 6 tests — unified createManualSession routing. |
|
|
705
|
+
| `tests/integration.test.ts` | 4 tests — end-to-end HTTP + SSE streaming. |
|
|
706
|
+
| `tests/clone.test.ts` | 8 tests — payload cloning. |
|
|
707
|
+
| `tests/detect.test.ts` | 4 tests — SSE content-type detection. |
|
|
708
|
+
|
|
709
|
+
## License
|
|
710
|
+
|
|
711
|
+
MIT
|