@powerduck/openapi-request 0.2.2 → 0.2.3

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.
Files changed (2) hide show
  1. package/README.md +316 -600
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,681 +1,390 @@
1
1
  # @powerduck/openapi-request
2
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.
3
+ Execute OpenAPI operations with full parameter serialization, security resolution, and response parsing. Supports fetch, axios, and custom HTTP clients. Built for browsers, Node.js, and Edge Functions.
5
4
 
6
- ## Features
5
+ [![npm version](https://img.shields.io/npm/v/@powerduck/openapi-request)](https://www.npmjs.com/package/@powerduck/openapi-request)
6
+ [![license](https://img.shields.io/npm/l/@powerduck/openapi-request)](https://github.com/PowerDuckie/openapi-request/blob/main/LICENSE)
7
+
8
+ ## Links
9
+
10
+ - [Official Website](https://www.powerduck.com/opensource/openapi-request.html)
11
+ - [Documentation](https://www.powerduck.com/docs/openapi-request/introduction)
12
+ - [Live Demo](https://www.powerduck.com/demo/openapi-request.html)
13
+ - [GitHub](https://github.com/PowerDuckie/openapi-request)
14
+ - [npm](https://www.npmjs.com/package/@powerduck/openapi-request)
15
+
16
+ ---
7
17
 
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.
18
+ ## Quick Start
16
19
 
17
- ## Installation
20
+ ### Install
18
21
 
19
22
  ```bash
20
23
  npm install @powerduck/openapi-request
21
24
  ```
22
25
 
23
- Requires Node.js >= 18.17.
24
-
25
- ## Quick Start
26
+ ### Create a client and send a request
26
27
 
27
- ```ts
28
+ ```typescript
28
29
  import { createClient } from "@powerduck/openapi-request";
29
30
 
30
31
  const client = createClient();
31
32
 
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
33
  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
34
+ spec: openApiDocument,
35
+ operationId: "getUserById",
36
+ parameters: {
37
+ path: { id: "123" },
38
+ query: { include: "profile" },
51
39
  },
52
40
  });
53
41
 
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();
42
+ console.log(result.response.status); // e.g. 200
43
+ console.log(result.response.body); // parsed response body
44
+ console.log(result.response.headers); // response headers
60
45
  ```
61
46
 
62
- ---
63
-
64
- ## API Reference
47
+ ### Prepare and send separately
65
48
 
66
- ### `createClient(options?)`
49
+ ```typescript
50
+ import { createClient } from "@powerduck/openapi-request";
67
51
 
68
- Returns a client with `prepare`, `send`, `sendMany`, `connect`, `discover`, `writeback`, `dispose`, `probeStreamingResponse`.
52
+ const client = createClient();
69
53
 
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. |
54
+ // Prepare builds the request without sending
55
+ const prepared = client.prepare({
56
+ spec: openApiDocument,
57
+ operationId: "listUsers",
58
+ parameters: {
59
+ query: { page: 1, limit: 20 },
60
+ },
61
+ });
74
62
 
75
- ### `client.prepare(options)` `PreparedRequest`
63
+ console.log(prepared.url); // final URL with query string
64
+ console.log(prepared.method); // HTTP method
65
+ console.log(prepared.headers); // resolved headers
76
66
 
77
- Pure, synchronous. No I/O. Returns everything the UI needs to choose a renderer.
67
+ // Send the prepared request
68
+ const result = await client.send(prepared);
69
+ ```
78
70
 
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. |
71
+ ### With security values
88
72
 
89
- ### `client.send(options)` → `Promise<SendResult>`
73
+ ```typescript
74
+ import { createClient } from "@powerduck/openapi-request";
90
75
 
91
- One-shot execution through the full pipeline: build collection → run → parse → write back.
76
+ const client = createClient({
77
+ securityValues: {
78
+ bearerAuth: "YOUR_ACCESS_TOKEN",
79
+ apiKey: "YOUR_API_KEY",
80
+ },
81
+ });
92
82
 
93
- ### `client.connect(options)` → `ManualSession`
83
+ const result = await client.send({
84
+ spec: openApiDocument,
85
+ operationId: "createUser",
86
+ parameters: {
87
+ body: { name: "Ada", email: "ada@example.com" },
88
+ },
89
+ });
90
+ ```
94
91
 
95
- Opens a long-lived session for WebSocket, gRPC or MCP. See [Manual Sessions](#manual-sessions).
92
+ ### Custom fetch function
96
93
 
97
- ### `client.discover(options)` → `Promise<DiscoveryResult>`
94
+ ```typescript
95
+ import { createClient } from "@powerduck/openapi-request";
98
96
 
99
- Discovers schema/capabilities for MCP or gRPC.
97
+ const client = createClient({
98
+ fetchFn: async (url, init) => {
99
+ console.log("Request:", init.method, url);
100
+ const response = await fetch(url, init);
101
+ console.log("Response:", response.status);
102
+ return response;
103
+ },
104
+ });
105
+ ```
100
106
 
101
- | Protocol | Options |
102
- |----------|---------|
103
- | `mcp` | `{ protocol: "mcp", endpoint, headers?, transport?, command?, args?, cwd? }` |
104
- | `grpc` | `{ protocol: "grpc", address, reflection?, protoPaths?, includeDirs?, metadata?, channelOptions? }` |
107
+ ---
105
108
 
106
- ### `client.writeback(spec, prepared, result, options?)` → `OpenApiDocument`
109
+ ## Features
107
110
 
108
- Merges a result's response into the spec at the operation's path+method.
111
+ - **Full OpenAPI parameter serialization** path, query, header, cookie parameters with all `style` and `explode` combinations (form, spaceDelimited, pipeDelimited, label, matrix, simple, deepObject)
112
+ - **Security resolution** — Bearer, Basic, API key (header/query/cookie), with per-request or client-level `securityValues`
113
+ - **Request body handling** — JSON, form-urlencoded, multipart/form-data (text + file fields), XML, text, binary
114
+ - **Response parsing** — automatic JSON parsing, text fallback, content-type detection
115
+ - **Two-phase execution** — `prepare()` builds the request without sending, `send()` executes it
116
+ - **Custom HTTP client** — plug in fetch, axios, node-fetch, or any compatible function
117
+ - **Server URL selection** — override or select from spec `servers` array
118
+ - **Timeout and abort** — per-request timeout with `AbortController` integration
119
+ - **Postman script support** — run `pm.test()` / `pm.expect()` scripts from `x-postman-scripts`
120
+ - **Batch requests** — send multiple operations with configurable concurrency
121
+ - **Browser and Node.js compatible** — works in browsers, Edge Functions, and Node.js
122
+ - **Dual ESM/CJS builds** — works with `import` and `require`, with bundled TypeScript declarations
109
123
 
110
124
  ---
111
125
 
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. |
126
+ ## API
152
127
 
153
- ---
128
+ ### `createClient(options?: CreateClientOptions): OpenApiClient`
154
129
 
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
- }
130
+ Creates a new client instance.
131
+
132
+ ```typescript
133
+ interface CreateClientOptions {
134
+ fetchFn?: (url: string, init: RequestInit) => Promise<Response>;
135
+ securityValues?: Record<string, string>;
136
+ serverUrl?: string;
137
+ timeoutMs?: number;
138
+ validateResponse?: boolean;
193
139
  }
194
140
  ```
195
141
 
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.
142
+ ### `client.send(options: SendOptions): Promise<SendResult>`
143
+
144
+ Prepares and sends a request in one call.
145
+
146
+ ### `client.prepare(options: SendOptions): PreparedRequest`
147
+
148
+ Builds the request without sending. Returns a `PreparedRequest` that can be inspected or passed to `send()`.
149
+
150
+ ### `SendOptions`
151
+
152
+ ```typescript
153
+ interface SendOptions {
154
+ spec: OpenApiDocument; // Parsed OpenAPI 3.2 document
155
+ operationId?: string; // Operation ID (alternative to path + method)
156
+ path?: string; // Path template, e.g. "/users/{id}"
157
+ method?: string; // HTTP method, e.g. "get"
158
+ parameters?: {
159
+ path?: Record<string, unknown>;
160
+ query?: Record<string, unknown>;
161
+ header?: Record<string, string>;
162
+ cookie?: Record<string, string>;
163
+ body?: unknown;
164
+ };
165
+ securityValues?: Record<string, string>;
166
+ serverUrl?: string;
167
+ timeoutMs?: number;
168
+ signal?: AbortSignal;
169
+ }
276
170
  ```
277
171
 
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
- }
172
+ > Either `operationId` or `path` + `method` must be provided to identify the operation.
173
+
174
+ ### `SendResult`
175
+
176
+ ```typescript
177
+ interface SendResult extends ExecResult {
178
+ response: {
179
+ status: number; // HTTP status code
180
+ statusText: string; // HTTP status text
181
+ headers: Record<string, string>; // Response headers
182
+ body?: unknown; // Parsed JSON body (if content-type is JSON)
183
+ text?: string; // Raw response text
184
+ };
185
+ timings: {
186
+ durationMs: number; // Total request duration in milliseconds
187
+ };
188
+ sizeBytes: number; // Response body size in bytes
299
189
  }
300
190
  ```
301
191
 
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
- });
192
+ > **Important:** Response data is under `result.response`, not directly on `result`. Use `result.response.status`, `result.response.body`, and `result.response.headers`.
332
193
 
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
- });
194
+ ### `PreparedRequest`
338
195
 
339
- await session.open();
340
- await session.send({ type: "subscribe", channel: "updates" });
341
- await session.close({ code: 1000, reason: "done" });
196
+ ```typescript
197
+ interface PreparedRequest {
198
+ url: string; // Final URL with query string
199
+ method: string; // HTTP method
200
+ headers: Record<string, string>; // Resolved headers
201
+ body?: BodyInit; // Request body (if any)
202
+ operationId?: string; // Resolved operation ID
203
+ path: string; // Path template
204
+ parameters: ResolvedParameters;
205
+ }
342
206
  ```
343
207
 
344
208
  ---
345
209
 
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
- ```
210
+ ## Parameter Serialization
211
+
212
+ All OpenAPI 3.2 parameter styles are supported:
377
213
 
378
- ### `graphql` options
214
+ | Location | Style | `explode: true` | `explode: false` |
215
+ |---|---|---|---|
216
+ | `path` | `simple` (default) | `id=5,role=admin` | `id,role=5,admin` |
217
+ | `path` | `label` | `.id=5.role=admin` | `.id,role=5,admin` |
218
+ | `path` | `matrix` | `;id=5;role=admin` | `;id,role=5,admin` |
219
+ | `query` | `form` (default) | `id=5&role=admin` | `id=5,role=admin` |
220
+ | `query` | `spaceDelimited` | `id=5%20role=admin` | `id=5%20role=admin` |
221
+ | `query` | `pipeDelimited` | `id=5\|role=admin` | `id=5\|role=admin` |
222
+ | `query` | `deepObject` | `user[id]=5&user[role]=admin` | — |
223
+ | `header` | `simple` (default) | `id=5,role=admin` | `id,role=5,admin` |
224
+ | `cookie` | `form` (default) | `id=5; role=admin` | `id=5,role=admin` |
225
+
226
+ ---
379
227
 
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. |
228
+ ## Security Schemes
388
229
 
389
- ### GraphQL discovery (introspection)
230
+ ### Bearer token
390
231
 
391
- ```ts
392
- const result = await client.discover({
393
- protocol: "graphql",
394
- endpoint: "https://countries.trevorblades.com/",
395
- headers: { Authorization: "Bearer xxx" },
232
+ ```typescript
233
+ const result = await client.send({
234
+ spec,
235
+ operationId: "getUser",
236
+ securityValues: { bearerAuth: "YOUR_TOKEN" },
396
237
  });
397
- // result.discovery contains the introspected schema
238
+ // Sends: Authorization: Bearer YOUR_TOKEN
398
239
  ```
399
240
 
400
- ---
241
+ ### Basic auth
401
242
 
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
- }
243
+ ```typescript
244
+ const result = await client.send({
245
+ spec,
246
+ operationId: "getUser",
247
+ securityValues: { basicAuth: "username:password" },
248
+ });
249
+ // Sends: Authorization: Basic <base64(username:password)>
425
250
  ```
426
251
 
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
- });
252
+ ### API key (header)
487
253
 
488
- session.onEvent((e) => {
489
- if (e.kind === "status") {
490
- console.log("status:", e.meta.code, e.meta.statusName);
491
- }
254
+ ```typescript
255
+ const result = await client.send({
256
+ spec,
257
+ operationId: "getUser",
258
+ securityValues: { apiKeyHeader: "YOUR_KEY" },
492
259
  });
260
+ // Sends: X-API-Key: YOUR_KEY (header name from spec)
261
+ ```
493
262
 
494
- await session.open();
495
- await session.send({ text: "hello" });
496
- // session.state === "closed" after a unary call
263
+ ### API key (query)
264
+
265
+ ```typescript
266
+ const result = await client.send({
267
+ spec,
268
+ operationId: "getUser",
269
+ securityValues: { apiKeyQuery: "YOUR_KEY" },
270
+ });
271
+ // Appends: ?api_key=YOUR_KEY (param name from spec)
497
272
  ```
498
273
 
499
274
  ---
500
275
 
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
- }
276
+ ## Request Body Examples
277
+
278
+ ### JSON
279
+
280
+ ```typescript
281
+ const result = await client.send({
282
+ spec,
283
+ operationId: "createUser",
284
+ parameters: {
285
+ body: { name: "Ada", email: "ada@example.com" },
286
+ },
287
+ });
524
288
  ```
525
289
 
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",
290
+ ### Form URL-encoded
291
+
292
+ ```typescript
293
+ const result = await client.send({
294
+ spec,
295
+ operationId: "login",
296
+ parameters: {
297
+ body: { username: "ada", password: "secret" },
298
+ },
554
299
  });
300
+ ```
555
301
 
556
- await session.open();
302
+ ### Multipart Form Data
557
303
 
558
- // List capabilities
559
- const tools = await session.listTools();
560
- const prompts = await session.listPrompts();
561
- const resources = await session.listResources();
304
+ ```typescript
305
+ const result = await client.send({
306
+ spec,
307
+ operationId: "uploadFile",
308
+ parameters: {
309
+ body: {
310
+ title: "My Document",
311
+ file: new File(["content"], "doc.pdf", { type: "application/pdf" }),
312
+ },
313
+ },
314
+ });
315
+ ```
562
316
 
563
- // Call a tool
564
- const result = await session.callTool("get_weather", { city: "SF" });
317
+ ---
565
318
 
566
- // Raw JSON-RPC
567
- const raw = await session.request("tools/call", { name: "x", arguments: {} });
319
+ ## Batch Requests
568
320
 
569
- await session.close();
570
- ```
321
+ ```typescript
322
+ import { createClient } from "@powerduck/openapi-request";
571
323
 
572
- ### MCP stdio session
324
+ const client = createClient();
573
325
 
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",
326
+ const results = await client.sendBatch([
327
+ { spec, operationId: "getUser", parameters: { path: { id: "1" } } },
328
+ { spec, operationId: "getUser", parameters: { path: { id: "2" } } },
329
+ { spec, operationId: "getUser", parameters: { path: { id: "3" } } },
330
+ ], { concurrency: 2 });
331
+
332
+ results.forEach((r, i) => {
333
+ if ("error" in r) {
334
+ console.log(`Request ${i} failed:`, r.error);
335
+ } else {
336
+ console.log(`Request ${i} status:`, r.response.status);
337
+ }
581
338
  });
582
- await session.open();
583
339
  ```
584
340
 
585
341
  ---
586
342
 
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.). |
343
+ ## Postman Scripts
616
344
 
617
- ---
345
+ Run Postman test scripts embedded in `x-postman-scripts`:
346
+
347
+ ```typescript
348
+ import { createClient, runPostmanScripts } from "@powerduck/openapi-request";
349
+
350
+ const client = createClient();
351
+ const result = await client.send({
352
+ spec,
353
+ operationId: "listUsers",
354
+ });
618
355
 
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. |
356
+ // Run pm.test() scripts from the operation's x-postman-scripts
357
+ const testResults = runPostmanScripts(spec, "listUsers", result.response);
358
+
359
+ console.log("Passed:", testResults.passed);
360
+ console.log("Failed:", testResults.failed);
361
+ for (const test of testResults.tests) {
362
+ console.log(`${test.passed ? "✓" : "✗"} ${test.name}`);
363
+ }
364
+ ```
647
365
 
648
366
  ---
649
367
 
650
368
  ## Error Handling
651
369
 
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
-
370
+ ```typescript
674
371
  try {
675
- await client.send(options);
676
- } catch (err) {
677
- if (err instanceof ProtoKitError) {
678
- console.error(err.code, err.message);
372
+ const result = await client.send({
373
+ spec,
374
+ operationId: "getUser",
375
+ parameters: { path: { id: "123" } },
376
+ });
377
+
378
+ if (result.response.status >= 400) {
379
+ console.error("API error:", result.response.status, result.response.body);
380
+ }
381
+ } catch (error) {
382
+ if (error.name === "AbortError") {
383
+ console.error("Request timed out or was aborted");
384
+ } else if (error.name === "OpenApiRequestError") {
385
+ console.error("Request preparation failed:", error.message);
386
+ } else {
387
+ console.error("Network error:", error);
679
388
  }
680
389
  }
681
390
  ```
@@ -685,26 +394,33 @@ try {
685
394
  ## Development
686
395
 
687
396
  ```bash
397
+ # Install dependencies
688
398
  npm install
689
- npm run build # tsup → dist/
690
- npm test # vitest run (107 tests)
691
- npm run typecheck # tsc --noEmit
399
+
400
+ # Type check
401
+ npm run typecheck
402
+
403
+ # Build (ESM + CJS + type declarations)
404
+ npm run build
405
+
406
+ # Run tests
407
+ npm test
408
+
409
+ # Watch mode
410
+ npx vitest watch
692
411
  ```
693
412
 
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. |
413
+ ---
414
+
415
+ ## Related Packages
416
+
417
+ - [`@powerduck/openapi-parser`](https://www.npmjs.com/package/@powerduck/openapi-parser) OpenAPI 3.2 parser, validator, and upgrader
418
+ - [`@powerduck/openapi-codegen`](https://www.npmjs.com/package/@powerduck/openapi-codegen) Generate runnable request examples in 21 languages
419
+ - [`@powerduck/openapi-cli`](https://www.npmjs.com/package/@powerduck/openapi-cli)CI-ready batch testing for OpenAPI documents
420
+ - [`@powerduck/openapi-mcp-server`](https://www.npmjs.com/package/@powerduck/openapi-mcp-server)Turn OpenAPI docs into MCP servers
421
+ - [`@powerduck/x-to-openapi`](https://www.npmjs.com/package/@powerduck/x-to-openapi) Convert curl commands and Postman Collections to OpenAPI 3.2
422
+
423
+ ---
708
424
 
709
425
  ## License
710
426
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@powerduck/openapi-request",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "OpenAPI 3.2 collection debugger with HTTP, SSE and WebSocket support, plus response write-back",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",