@powerduck/openapi-request 0.2.2 → 0.2.4

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 +192 -631
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,711 +1,272 @@
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
+ [![npm version](https://img.shields.io/npm/v/@powerduck/openapi-request)](https://www.npmjs.com/package/@powerduck/openapi-request)
4
+ [![license](https://img.shields.io/npm/l/@powerduck/openapi-request)](https://github.com/PowerDuckie/openapi-request/blob/main/LICENSE)
5
+ [![downloads](https://img.shields.io/npm/dm/@powerduck/openapi-request)](https://www.npmjs.com/package/@powerduck/openapi-request)
5
6
 
6
- ## Features
7
+ 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.
8
+
9
+ ---
10
+
11
+ Powerduck is an open-source developer tooling platform for teams building modern API workflows.
7
12
 
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.
13
+ - **Full Parameter Serialization** Path, query, header, and cookie parameters with RFC 6570 URI templates
14
+ - **Security Resolution** Bearer tokens, API keys, Basic auth, OAuth2, and custom schemes
15
+ - **3 HTTP Clients** fetch (default), axios, and custom client adapters
16
+ - **Request Body Handling** JSON, form-data, x-www-form-urlencoded, and raw payloads
17
+ - **Response Parsing** — Automatic JSON, text, blob, and stream parsing with content-type detection
18
+ - **Prepare & Send** Two-phase API for request inspection before sending
19
+ - **Type-Safe Operations** Full TypeScript types for parameters, request bodies, and responses
20
+ - **Error Handling** — Structured error objects with status, headers, and parsed body
21
+ - **Interceptors** — Request and response interceptors for logging, auth refresh, and retries
22
+ - **Browser & Node** — Works in browsers, Node.js, and Edge Functions with zero dependencies
16
23
 
17
- ## Installation
24
+ ---
25
+
26
+ ## Quick Start
27
+
28
+ ### Install
18
29
 
19
30
  ```bash
20
31
  npm install @powerduck/openapi-request
21
32
  ```
22
33
 
23
- Requires Node.js >= 18.17.
24
-
25
- ## Quick Start
34
+ ### Create a client and send a request
26
35
 
27
- ```ts
36
+ ```typescript
28
37
  import { createClient } from "@powerduck/openapi-request";
29
38
 
30
39
  const client = createClient();
31
40
 
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
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
42
+ spec: openApiDocument,
43
+ operationId: "getUserById",
44
+ parameters: {
45
+ path: { id: "123" },
46
+ query: { include: "profile" },
51
47
  },
52
48
  });
53
49
 
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();
50
+ console.log(result.response.status); // e.g. 200
51
+ console.log(result.response.body); // parsed response body
52
+ console.log(result.response.headers); // response headers
60
53
  ```
61
54
 
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>`
55
+ ### Prepare and send separately
90
56
 
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? }` |
57
+ ```typescript
58
+ import { createClient } from "@powerduck/openapi-request";
105
59
 
106
- ### `client.writeback(spec, prepared, result, options?)` → `OpenApiDocument`
60
+ const client = createClient();
107
61
 
108
- Merges a result's response into the spec at the operation's path+method.
62
+ // Prepare builds the request without sending
63
+ const prepared = client.prepare({
64
+ spec: openApiDocument,
65
+ operationId: "listUsers",
66
+ parameters: {
67
+ query: { page: 1, limit: 20 },
68
+ },
69
+ });
109
70
 
110
- ---
71
+ console.log(prepared.url); // final URL with serialized params
72
+ console.log(prepared.method); // HTTP method
73
+ console.log(prepared.headers); // resolved headers
111
74
 
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. |
75
+ // Send the prepared request
76
+ const result = await client.sendPrepared(prepared);
77
+ ```
152
78
 
153
- ---
79
+ ### With authentication
154
80
 
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
- ```
81
+ ```typescript
82
+ import { createClient } from "@powerduck/openapi-request";
195
83
 
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
84
+ const client = createClient({
85
+ securityValues: {
86
+ bearerAuth: "your-token-here",
87
+ apiKey: "your-api-key",
271
88
  },
272
89
  });
273
90
 
274
- // result.response.events contains all retained events.
275
- // result.response.truncated / stopReason explain why sampling ended.
91
+ const result = await client.send({
92
+ spec: openApiDocument,
93
+ operationId: "getProfile",
94
+ });
276
95
  ```
277
96
 
278
97
  ---
279
98
 
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
- ```
99
+ ## Links
301
100
 
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
- });
101
+ - [Official Website](https://www.powerduck.com/opensource/openapi-request.html)
102
+ - [Documentation](https://www.powerduck.com/docs/openapi-request/introduction)
103
+ - [GitHub](https://github.com/PowerDuckie/openapi-request)
104
+ - [npm](https://www.npmjs.com/package/@powerduck/openapi-request)
332
105
 
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
- });
106
+ ---
338
107
 
339
- await session.open();
340
- await session.send({ type: "subscribe", channel: "updates" });
341
- await session.close({ code: 1000, reason: "done" });
342
- ```
108
+ ## Features
109
+
110
+ - **Full parameter serialization** — Path, query, header, and cookie parameters with RFC 6570 URI templates
111
+ - **Security resolution** — Bearer tokens, API keys, Basic auth, OAuth2, and custom schemes
112
+ - **3 HTTP clients** — fetch (default), axios, and custom client adapters
113
+ - **Request body handling** — JSON, form-data, x-www-form-urlencoded, and raw payloads
114
+ - **Response parsing** — Automatic JSON, text, blob, and stream parsing with content-type detection
115
+ - **Prepare & send** — Two-phase API for request inspection before sending
116
+ - **Type-safe operations** — Full TypeScript types for parameters, request bodies, and responses
117
+ - **Error handling** — Structured error objects with status, headers, and parsed body
118
+ - **Interceptors** — Request and response interceptors for logging, auth refresh, and retries
119
+ - **Browser & Node** — Works in browsers, Node.js, and Edge Functions with zero dependencies
120
+ - **Server selection** — Auto-select or manually specify server from OpenAPI servers
121
+ - **Content negotiation** — Automatic Accept header based on response content types
122
+ - **Upload progress** — Progress callbacks for file uploads with axios client
123
+ - **Abort support** — AbortController / AbortSignal for request cancellation
124
+ - **Dual ESM/CJS** — Works with `import` and `require`, with bundled TypeScript declarations
343
125
 
344
126
  ---
345
127
 
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
- ```
128
+ ## API Reference
377
129
 
378
- ### `graphql` options
130
+ ### `createClient(options?)`
379
131
 
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. |
132
+ Create an OpenAPI request client.
388
133
 
389
- ### GraphQL discovery (introspection)
134
+ ```typescript
135
+ import { createClient } from "@powerduck/openapi-request";
390
136
 
391
- ```ts
392
- const result = await client.discover({
393
- protocol: "graphql",
394
- endpoint: "https://countries.trevorblades.com/",
395
- headers: { Authorization: "Bearer xxx" },
137
+ const client = createClient({
138
+ client: "fetch", // "fetch" | "axios" | custom adapter
139
+ baseUrl: "https://api.example.com",
140
+ securityValues: { bearerAuth: "token" },
141
+ defaultHeaders: { "X-App": "my-app" },
142
+ timeout: 30000,
396
143
  });
397
- // result.discovery contains the introspected schema
398
144
  ```
399
145
 
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
- ```
146
+ ### `client.send(options)`
426
147
 
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
- });
148
+ Send an OpenAPI operation.
487
149
 
488
- session.onEvent((e) => {
489
- if (e.kind === "status") {
490
- console.log("status:", e.meta.code, e.meta.statusName);
491
- }
150
+ ```typescript
151
+ const result = await client.send({
152
+ spec: openApiDocument,
153
+ operationId: "getUser",
154
+ parameters: {
155
+ path: { id: "123" },
156
+ query: { include: ["profile", "orders"] },
157
+ header: { "X-Request-ID": "abc" },
158
+ },
159
+ requestBody: { name: "Ada" },
160
+ securityValues: { bearerAuth: "token" },
161
+ serverIndex: 0,
162
+ signal: abortSignal,
492
163
  });
493
-
494
- await session.open();
495
- await session.send({ text: "hello" });
496
- // session.state === "closed" after a unary call
497
164
  ```
498
165
 
499
- ---
166
+ ### `client.prepare(options)`
500
167
 
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
- ```
168
+ Prepare a request without sending.
525
169
 
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",
170
+ ```typescript
171
+ const prepared = client.prepare({
172
+ spec: openApiDocument,
173
+ operationId: "listUsers",
174
+ parameters: { query: { page: 1 } },
554
175
  });
555
176
 
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();
177
+ // prepared.url, prepared.method, prepared.headers, prepared.body
178
+ ```
562
179
 
563
- // Call a tool
564
- const result = await session.callTool("get_weather", { city: "SF" });
180
+ ### `client.sendPrepared(prepared)`
565
181
 
566
- // Raw JSON-RPC
567
- const raw = await session.request("tools/call", { name: "x", arguments: {} });
182
+ Send a previously prepared request.
568
183
 
569
- await session.close();
184
+ ```typescript
185
+ const result = await client.sendPrepared(prepared);
570
186
  ```
571
187
 
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();
188
+ ### Result Type
189
+
190
+ ```typescript
191
+ interface RequestResult<T = unknown> {
192
+ response: {
193
+ status: number;
194
+ statusText: string;
195
+ headers: Record<string, string>;
196
+ body: T;
197
+ raw: Response;
198
+ };
199
+ request: {
200
+ url: string;
201
+ method: string;
202
+ headers: Record<string, string>;
203
+ body?: unknown;
204
+ };
205
+ duration: number; // milliseconds
206
+ }
583
207
  ```
584
208
 
585
- ---
209
+ ### Error Type
586
210
 
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.). |
211
+ ```typescript
212
+ class OpenApiRequestError extends Error {
213
+ status: number;
214
+ statusText: string;
215
+ headers: Record<string, string>;
216
+ body: unknown;
217
+ request: { url: string; method: string; headers: Record<string, string> };
218
+ }
219
+ ```
616
220
 
617
221
  ---
618
222
 
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. |
223
+ ## Interceptors
647
224
 
648
- ---
225
+ ```typescript
226
+ import { createClient } from "@powerduck/openapi-request";
649
227
 
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
- }
228
+ const client = createClient({
229
+ interceptors: {
230
+ request: async (request) => {
231
+ console.log("Request:", request.method, request.url);
232
+ request.headers["X-Request-ID"] = crypto.randomUUID();
233
+ return request;
234
+ },
235
+ response: async (response) => {
236
+ console.log("Response:", response.status);
237
+ return response;
238
+ },
239
+ error: async (error) => {
240
+ if (error.status === 401) {
241
+ // Refresh token and retry
242
+ return refreshToken().then(() => client.sendPrepared(error.request));
243
+ }
244
+ throw error;
245
+ },
246
+ },
247
+ });
681
248
  ```
682
249
 
683
250
  ---
684
251
 
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
252
+ ## TypeScript Types
253
+
254
+ ```typescript
255
+ import type {
256
+ OpenApiClient,
257
+ ClientOptions,
258
+ SendOptions,
259
+ PrepareOptions,
260
+ RequestResult,
261
+ OpenApiRequestError,
262
+ ParameterMap,
263
+ SecurityValues,
264
+ HttpMethod,
265
+ } from "@powerduck/openapi-request";
692
266
  ```
693
267
 
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. |
268
+ ---
708
269
 
709
270
  ## License
710
271
 
711
- MIT
272
+ MIT © [POWERDUCK LIMITED](https://www.powerduck.com)
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.4",
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",