zuplo 6.73.21 → 6.73.23

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.
@@ -10,20 +10,25 @@ us to trial this or sign up for an Enterprise account.
10
10
 
11
11
  :::
12
12
 
13
- Zuplo provides two handlers for proxying WebSocket connections to your backend
14
- WebSocket APIs:
15
-
16
- - **`webSocketHandler`** proxies WebSocket traffic straight through to your
17
- backend without inspecting the messages. Use this when you only need to
18
- authenticate, rate limit, or route the connection.
19
- - **`webSocketPipelineHandler`** does everything `webSocketHandler` does and
20
- additionally runs every message through a policy pipeline, so you can inspect,
21
- transform, or drop individual messages in either direction. See the
22
- [WebSocket Pipeline Handler](./websocket-pipeline-handler.mdx).
13
+ :::caution{title="Deprecated"}
14
+
15
+ `webSocketHandler` is deprecated. It's now an alias of
16
+ [`webSocketPipelineHandler`](./websocket-pipeline-handler.mdx), which does
17
+ everything this handler does with no message policies configured it's the same
18
+ transparent passthrough. Existing routes keep working unchanged; switch the
19
+ handler `export` name to `webSocketPipelineHandler` when convenient.
20
+
21
+ :::
22
+
23
+ The WebSocket Handler proxies WebSocket traffic straight through to your backend
24
+ without inspecting the messages. Use the
25
+ [WebSocket Pipeline Handler](./websocket-pipeline-handler.mdx) when you also
26
+ want to intercept individual messages or run an
27
+ [`onOpen` connection hook](./websocket-pipeline-handler.mdx#the-onopen-connection-hook).
23
28
 
24
- Both handlers can be configured alongside other existing policies like
29
+ The handler can be configured alongside other existing policies like
25
30
  [Rate Limiting](../policies/rate-limit-inbound.mdx),
26
- [API Keys](../policies/api-key-inbound.mdx), etc. and are available for use on
31
+ [API Keys](../policies/api-key-inbound.mdx), etc. and is available for use on
27
32
  all environments.
28
33
 
29
34
  These handlers are only configurable via the JSON View on a project's Route
@@ -75,9 +80,12 @@ The WebSocket Handler accepts the following options in the `options` property:
75
80
  - **`rewritePattern`** (required): The URL pattern for the backend WebSocket
76
81
  endpoint. Supports JavaScript string interpolation syntax for dynamic URL
77
82
  construction based on request data and environment variables.
78
- - **`policies`** (optional, `webSocketPipelineHandler` only): Configures the
79
- message-interception policies that run on each WebSocket frame. See the
83
+ - **`policies`** (optional): Configures the message-interception policies that
84
+ run on each WebSocket frame. See the
80
85
  [WebSocket Pipeline Handler](./websocket-pipeline-handler.mdx).
86
+ - **`onOpen`** (optional): Configures a connection hook that runs once per
87
+ connection, before any message is relayed. See
88
+ [the `onOpen` connection hook](./websocket-pipeline-handler.mdx#the-onopen-connection-hook).
81
89
 
82
90
  Similar to other handlers using `rewritePattern`, it supports JavaScript string
83
91
  interpolation syntax and can be used to shape the URL based on data from the
@@ -10,12 +10,24 @@ to trial this or sign up for an Enterprise account.
10
10
 
11
11
  :::
12
12
 
13
- The `webSocketPipelineHandler` proxies WebSocket connections exactly like the
14
- [WebSocket Handler](./websocket-handler.mdx), but additionally passes every
15
- WebSocket message through a pipeline of **policy functions** before forwarding
16
- it. Each policy can inspect, transform, or drop the message. Use this to redact
17
- sensitive fields, enforce a message schema, filter events, or add observability
18
- to real-time traffic.
13
+ The `webSocketPipelineHandler` is Zuplo's WebSocket handler. It proxies
14
+ WebSocket connections to your backend and optionally lets you run code at two
15
+ points in the connection's life:
16
+
17
+ - **Message policies** pass every WebSocket message through a pipeline of
18
+ **policy functions** before forwarding it. Each policy can inspect, transform,
19
+ or drop the message. Use this to redact sensitive fields, enforce a message
20
+ schema, filter events, or add observability to real-time traffic.
21
+ - **The [`onOpen` connection hook](#the-onopen-connection-hook)** runs once per
22
+ connection, after the connection to your backend is established and before any
23
+ message is relayed. Use it to gate connections, send a handshake message to
24
+ your backend, or attach your own connection lifecycle listeners (such as
25
+ `close`).
26
+
27
+ With no policies and no `onOpen` hook configured, the handler is a transparent
28
+ passthrough — this is what the deprecated
29
+ [`webSocketHandler`](./websocket-handler.mdx) did, and it now resolves to this
30
+ handler.
19
31
 
20
32
  Messages are intercepted in both directions, configured independently:
21
33
 
@@ -28,8 +40,10 @@ Messages are intercepted in both directions, configured independently:
28
40
 
29
41
  The pipeline only intercepts **message** events. Connection lifecycle events are
30
42
  handled automatically: when either side closes, the other side is closed, and
31
- socket errors are logged and forwarded. There is no policy hook for `close` or
32
- `error` events.
43
+ socket errors are logged and the connection is torn down. There is no dedicated
44
+ policy hook for `close` or `error` events — instead, use the
45
+ [`onOpen` hook](#the-onopen-connection-hook) to attach your own listeners to
46
+ either socket.
33
47
 
34
48
  :::
35
49
 
@@ -187,3 +201,126 @@ You can configure multiple policies in each direction to compose behavior — fo
187
201
  example, one policy to validate a message against a schema and a second to
188
202
  redact fields. Because each policy receives the previous policy's output, order
189
203
  matters.
204
+
205
+ ## The `onOpen` connection hook
206
+
207
+ The `onOpen` option configures a function that runs **once per connection**,
208
+ after both the client and backend connections are established and **before any
209
+ message is relayed**. Unlike message policies, it isn't tied to a direction — it
210
+ receives both sockets.
211
+
212
+ Configure it at the `options` level, as a sibling of `rewritePattern` and
213
+ `policies`. It works with or without message policies:
214
+
215
+ ```json
216
+ "handler": {
217
+ "export": "webSocketPipelineHandler",
218
+ "module": "$import(@zuplo/runtime)",
219
+ "options": {
220
+ "rewritePattern": "https://myservice.com/websocket",
221
+ "onOpen": {
222
+ "module": "$import(./modules/websocket-hooks)",
223
+ "export": "onWebSocketOpen"
224
+ }
225
+ }
226
+ }
227
+ ```
228
+
229
+ The hook matches the `WebSocketOnOpenHook` type exported from `@zuplo/runtime` —
230
+ the same parameters as a message policy, minus the message data:
231
+
232
+ ```ts
233
+ import { ZuploContext, ZuploRequest } from "@zuplo/runtime";
234
+
235
+ type WebSocketOnOpenHook = (
236
+ target: WebSocket,
237
+ source: WebSocket,
238
+ request: ZuploRequest,
239
+ context: ZuploContext,
240
+ ) => void | Promise<void>;
241
+ ```
242
+
243
+ | Parameter | Description |
244
+ | --------- | ------------------------------------------------------------------------------------------------- |
245
+ | `target` | The backend connection. Call `target.send(...)` to send a message to your backend. |
246
+ | `source` | The client connection. Call `source.send(...)` to send a message to the client. |
247
+ | `request` | The original [`ZuploRequest`](../programmable-api/zuplo-request.mdx) from the connection upgrade. |
248
+ | `context` | The [`ZuploContext`](../programmable-api/zuplo-context.mdx) for the connection. |
249
+
250
+ The hook is awaited, and its outcome controls the connection:
251
+
252
+ - **Return (or resolve)** to accept the connection. Message relaying begins only
253
+ after the hook completes — messages that arrive while it runs are buffered and
254
+ delivered afterward, in order.
255
+ - **Throw (or reject)** to abort the connection. Both sockets are closed and the
256
+ client never receives the `101 Switching Protocols` upgrade response.
257
+
258
+ ### Example: gate connections and greet the backend
259
+
260
+ This hook rejects unauthenticated connections, then sends a handshake message to
261
+ the backend before any client traffic flows.
262
+
263
+ ```ts
264
+ import { WebSocketOnOpenHook } from "@zuplo/runtime";
265
+
266
+ export const onWebSocketOpen: WebSocketOnOpenHook = async (
267
+ target,
268
+ source,
269
+ request,
270
+ context,
271
+ ) => {
272
+ if (!request.user) {
273
+ // Aborts the upgrade: both sockets close and no 101 is returned
274
+ throw new Error("Unauthenticated WebSocket connection");
275
+ }
276
+
277
+ // Runs before any client message is relayed to the backend
278
+ target.send(
279
+ JSON.stringify({ type: "session.start", user: request.user.sub }),
280
+ );
281
+ };
282
+ ```
283
+
284
+ ### Example: react to the connection closing
285
+
286
+ There is no separate `onClose` option — because `onOpen` hands you both raw
287
+ sockets, attach a standard `close` event listener to either one. Listeners
288
+ attached here live for the duration of the connection; there's no need to remove
289
+ them.
290
+
291
+ ```ts
292
+ import { WebSocketOnOpenHook } from "@zuplo/runtime";
293
+
294
+ export const trackConnection: WebSocketOnOpenHook = (
295
+ target,
296
+ source,
297
+ request,
298
+ context,
299
+ ) => {
300
+ const connectedAt = Date.now();
301
+
302
+ // Fires when the client disconnects
303
+ source.addEventListener("close", () => {
304
+ const seconds = Math.round((Date.now() - connectedAt) / 1000);
305
+ context.log.info(`Client disconnected after ${seconds}s`);
306
+ });
307
+
308
+ // Fires when your backend closes the connection
309
+ target.addEventListener("close", () => {
310
+ context.log.warn("Backend closed the WebSocket connection");
311
+ });
312
+ };
313
+ ```
314
+
315
+ The same pattern works for `error` and `message` events. For a listener that
316
+ should fire only once — such as waiting for a single handshake reply from your
317
+ backend — pass `{ once: true }` as the third argument to `addEventListener`.
318
+
319
+ :::tip{title="Performance"}
320
+
321
+ With no message policies and no `onOpen` hook, the gateway hands the connection
322
+ straight through to your backend and stays out of the per-message data path
323
+ entirely. Configuring `onOpen` (or any message policy) keeps the gateway in the
324
+ data path for that route, since the hook needs both live sockets to act on.
325
+
326
+ :::
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zuplo",
3
- "version": "6.73.21",
3
+ "version": "6.73.23",
4
4
  "type": "module",
5
5
  "description": "The programmable API Gateway",
6
6
  "author": "Zuplo, Inc.",
@@ -19,9 +19,9 @@
19
19
  "zuplo": "zuplo.js"
20
20
  },
21
21
  "dependencies": {
22
- "@zuplo/cli": "6.73.21",
23
- "@zuplo/core": "6.73.21",
24
- "@zuplo/runtime": "6.73.21",
22
+ "@zuplo/cli": "6.73.23",
23
+ "@zuplo/core": "6.73.23",
24
+ "@zuplo/runtime": "6.73.23",
25
25
  "@zuplo/test": "1.4.0"
26
26
  }
27
27
  }