zuplo 6.71.20 → 6.71.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/articles/graphql-caching.mdx +96 -0
- package/docs/articles/graphql-security.mdx +137 -126
- package/docs/articles/graphql-support.mdx +107 -0
- package/docs/articles/graphql.mdx +20 -0
- package/docs/dev-portal/zudoku/components/landing-page.mdx +7 -7
- package/docs/dev-portal/zudoku/components/link.mdx +5 -5
- package/docs/dev-portal/zudoku/configuration/footer.mdx +2 -2
- package/docs/dev-portal/zudoku/configuration/overview.md +1 -1
- package/docs/dev-portal/zudoku/guides/redirects.md +2 -2
- package/docs/handlers/websocket-handler.mdx +23 -7
- package/docs/handlers/websocket-pipeline-handler.mdx +189 -0
- package/docs/mcp-gateway/cross-app-access/overview.mdx +2 -3
- package/docs/mcp-gateway/introduction.mdx +0 -9
- package/docs/policies/_index.md +3 -1
- package/docs/policies/graphql-analytics-outbound/doc.md +10 -6
- package/docs/policies/graphql-analytics-outbound/schema.json +5 -4
- package/docs/policies/graphql-cache-inbound/doc.md +137 -0
- package/docs/policies/graphql-cache-inbound/intro.md +15 -0
- package/docs/policies/graphql-cache-inbound/schema.json +74 -0
- package/docs/policies/rate-limit-inbound/schema.json +8 -4
- package/docs/policies/traffic-splitting-inbound/doc.md +67 -0
- package/docs/policies/traffic-splitting-inbound/intro.md +5 -0
- package/docs/policies/traffic-splitting-inbound/schema.json +121 -0
- package/package.json +4 -4
|
@@ -59,7 +59,7 @@ footer: {
|
|
|
59
59
|
links: [
|
|
60
60
|
{ label: "Features", href: "/features" },
|
|
61
61
|
{ label: "Pricing", href: "/pricing" },
|
|
62
|
-
{ label: "Documentation", href: "/
|
|
62
|
+
{ label: "Documentation", href: "/" },
|
|
63
63
|
{ label: "GitHub", href: "https://github.com/org/repo" }, // Auto-detected as external
|
|
64
64
|
],
|
|
65
65
|
},
|
|
@@ -184,7 +184,7 @@ footer: {
|
|
|
184
184
|
links: [
|
|
185
185
|
{ label: "Features", href: "/features" },
|
|
186
186
|
{ label: "Pricing", href: "/pricing" },
|
|
187
|
-
{ label: "Documentation", href: "/
|
|
187
|
+
{ label: "Documentation", href: "/" }
|
|
188
188
|
]
|
|
189
189
|
},
|
|
190
190
|
{
|
|
@@ -46,7 +46,7 @@ const config: ZudokuConfig = {
|
|
|
46
46
|
},
|
|
47
47
|
{ type: "link", to: "api", label: "API Reference" },
|
|
48
48
|
],
|
|
49
|
-
redirects: [{ from: "/", to: "/
|
|
49
|
+
redirects: [{ from: "/", to: "/introduction" }],
|
|
50
50
|
apis: {
|
|
51
51
|
type: "file",
|
|
52
52
|
input: "./apis/openapi.yaml",
|
|
@@ -85,7 +85,7 @@ signal a permanent move; the JavaScript redirect used on generic static hosts is
|
|
|
85
85
|
The most common use of redirects is setting a landing page for the root path of your portal:
|
|
86
86
|
|
|
87
87
|
```ts
|
|
88
|
-
redirects: [{ from: "/", to: "/
|
|
88
|
+
redirects: [{ from: "/", to: "/introduction" }];
|
|
89
89
|
```
|
|
90
90
|
|
|
91
91
|
This sends visitors who arrive at your portal's root URL to your introduction page.
|
|
@@ -98,7 +98,7 @@ external links continue to work:
|
|
|
98
98
|
```ts
|
|
99
99
|
redirects: [
|
|
100
100
|
{ from: "/api/authentication", to: "/guides/auth-overview" },
|
|
101
|
-
{ from: "/api/getting-started", to: "/
|
|
101
|
+
{ from: "/api/getting-started", to: "/quickstart" },
|
|
102
102
|
{ from: "/reference", to: "/api" },
|
|
103
103
|
];
|
|
104
104
|
```
|
|
@@ -5,22 +5,35 @@ sidebar_label: WebSocket Handler
|
|
|
5
5
|
|
|
6
6
|
:::note
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
or sign up for an Enterprise account.
|
|
8
|
+
WebSocket handlers are an Enterprise-only feature at this time. Please contact
|
|
9
|
+
us to trial this or sign up for an Enterprise account.
|
|
10
10
|
|
|
11
11
|
:::
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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).
|
|
23
|
+
|
|
24
|
+
Both handlers can be configured alongside other existing policies like
|
|
25
|
+
[Rate Limiting](../policies/rate-limit-inbound.mdx),
|
|
26
|
+
[API Keys](../policies/api-key-inbound.mdx), etc. and are available for use on
|
|
17
27
|
all environments.
|
|
18
28
|
|
|
19
|
-
|
|
29
|
+
These handlers are only configurable via the JSON View on a project's Route
|
|
20
30
|
Designer or directly in your project's `*.oas.json` file.
|
|
21
31
|
|
|
22
32
|
## Setup in `routes.oas.json`
|
|
23
33
|
|
|
34
|
+
This section covers the passthrough `webSocketHandler`. To intercept messages,
|
|
35
|
+
see the [WebSocket Pipeline Handler](./websocket-pipeline-handler.mdx).
|
|
36
|
+
|
|
24
37
|
Configuration of the WebSocket Handler is similar to other available handlers.
|
|
25
38
|
Set the name of the path that your WebSocket API route will use, set the use of
|
|
26
39
|
the `webSocketHandler` export from `@zuplo/runtime` module in the handler
|
|
@@ -62,6 +75,9 @@ The WebSocket Handler accepts the following options in the `options` property:
|
|
|
62
75
|
- **`rewritePattern`** (required): The URL pattern for the backend WebSocket
|
|
63
76
|
endpoint. Supports JavaScript string interpolation syntax for dynamic URL
|
|
64
77
|
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
|
|
80
|
+
[WebSocket Pipeline Handler](./websocket-pipeline-handler.mdx).
|
|
65
81
|
|
|
66
82
|
Similar to other handlers using `rewritePattern`, it supports JavaScript string
|
|
67
83
|
interpolation syntax and can be used to shape the URL based on data from the
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: WebSocket Pipeline Handler
|
|
3
|
+
sidebar_label: WebSocket Pipeline Handler
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
:::note
|
|
7
|
+
|
|
8
|
+
The WebSocket Pipeline Handler is an Enterprise-only feature. Please contact us
|
|
9
|
+
to trial this or sign up for an Enterprise account.
|
|
10
|
+
|
|
11
|
+
:::
|
|
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.
|
|
19
|
+
|
|
20
|
+
Messages are intercepted in both directions, configured independently:
|
|
21
|
+
|
|
22
|
+
- **`inbound`** policies process messages traveling from the client to your
|
|
23
|
+
backend.
|
|
24
|
+
- **`outbound`** policies process messages traveling from your backend to the
|
|
25
|
+
client.
|
|
26
|
+
|
|
27
|
+
:::note
|
|
28
|
+
|
|
29
|
+
The pipeline only intercepts **message** events. Connection lifecycle events are
|
|
30
|
+
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.
|
|
33
|
+
|
|
34
|
+
:::
|
|
35
|
+
|
|
36
|
+
## Configuration
|
|
37
|
+
|
|
38
|
+
Use the `webSocketPipelineHandler` export and add an `inbound` and/or `outbound`
|
|
39
|
+
array under `options.policies`. Each entry points to an exported function in one
|
|
40
|
+
of your project's modules.
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
"/my-websocket": {
|
|
44
|
+
"x-zuplo-path": {
|
|
45
|
+
"pathMode": "open-api"
|
|
46
|
+
},
|
|
47
|
+
"get": {
|
|
48
|
+
"summary": "WebSocket route with message interception",
|
|
49
|
+
"x-zuplo-route": {
|
|
50
|
+
"corsPolicy": "none",
|
|
51
|
+
"handler": {
|
|
52
|
+
"export": "webSocketPipelineHandler",
|
|
53
|
+
"module": "$import(@zuplo/runtime)",
|
|
54
|
+
"options": {
|
|
55
|
+
"rewritePattern": "https://myservice.com/websocket",
|
|
56
|
+
"policies": {
|
|
57
|
+
"inbound": [
|
|
58
|
+
{
|
|
59
|
+
"module": "$import(./modules/websocket-policies)",
|
|
60
|
+
"export": "redactInbound"
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
"outbound": [
|
|
64
|
+
{
|
|
65
|
+
"module": "$import(./modules/websocket-policies)",
|
|
66
|
+
"export": "filterOutbound"
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"policies": {
|
|
73
|
+
"inbound": []
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"operationId": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
:::caution{title="Two different policies blocks"}
|
|
82
|
+
|
|
83
|
+
The `policies` object inside `handler.options` configures the **message**
|
|
84
|
+
policies that run on each WebSocket frame. This is separate from the route-level
|
|
85
|
+
`policies` block (the `inbound` array next to `handler`), which configures the
|
|
86
|
+
standard request policies — such as [API Key](../policies/api-key-inbound.mdx)
|
|
87
|
+
or [Rate Limiting](../policies/rate-limit-inbound.mdx) — that run once during
|
|
88
|
+
the initial connection upgrade.
|
|
89
|
+
|
|
90
|
+
:::
|
|
91
|
+
|
|
92
|
+
The `rewritePattern` option behaves identically to the
|
|
93
|
+
[WebSocket Handler](./websocket-handler.mdx#handler-options), including
|
|
94
|
+
JavaScript string interpolation.
|
|
95
|
+
|
|
96
|
+
## Writing a message policy
|
|
97
|
+
|
|
98
|
+
A message policy is an exported function that matches the following signature:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { ZuploContext, ZuploRequest } from "@zuplo/runtime";
|
|
102
|
+
|
|
103
|
+
async function webSocketPolicy(
|
|
104
|
+
data: string,
|
|
105
|
+
target: WebSocket,
|
|
106
|
+
source: WebSocket,
|
|
107
|
+
request: ZuploRequest,
|
|
108
|
+
context: ZuploContext,
|
|
109
|
+
): Promise<unknown>;
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
| Parameter | Description |
|
|
113
|
+
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
114
|
+
| `data` | The message payload. For text protocols this is a string; binary frames arrive in the platform's binary form (for example, an `ArrayBuffer`). For an `inbound` policy this is the message from the client; for an `outbound` policy it is the message from the backend. |
|
|
115
|
+
| `target` | The destination socket the message is being forwarded to. For `inbound` this is the backend connection; for `outbound` it is the client connection. |
|
|
116
|
+
| `source` | The socket the message originated from. Call `source.send(...)` to send a message back to the originator, such as an acknowledgement or error. |
|
|
117
|
+
| `request` | The original [`ZuploRequest`](../programmable-api/zuplo-request.mdx) from the connection upgrade. |
|
|
118
|
+
| `context` | The [`ZuploContext`](../programmable-api/zuplo-context.mdx) for the connection. |
|
|
119
|
+
|
|
120
|
+
The return value controls what happens next:
|
|
121
|
+
|
|
122
|
+
- **Return the data** (modified or unchanged) to forward it to `target`. When
|
|
123
|
+
multiple policies are configured, the return value is passed as `data` to the
|
|
124
|
+
next policy in the array.
|
|
125
|
+
- **Return `undefined`** to drop the message. It is not forwarded, and no
|
|
126
|
+
further policies run for that message.
|
|
127
|
+
|
|
128
|
+
Policies run in the order they appear in the `inbound` / `outbound` array, and
|
|
129
|
+
each policy may be asynchronous. If a policy throws, Zuplo logs the error and
|
|
130
|
+
drops the message.
|
|
131
|
+
|
|
132
|
+
## Example: redact fields from inbound messages
|
|
133
|
+
|
|
134
|
+
This inbound policy parses each JSON message from the client, removes a
|
|
135
|
+
sensitive field, and forwards the result to the backend.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { ZuploContext, ZuploRequest } from "@zuplo/runtime";
|
|
139
|
+
|
|
140
|
+
export async function redactInbound(
|
|
141
|
+
data: string,
|
|
142
|
+
target: WebSocket,
|
|
143
|
+
source: WebSocket,
|
|
144
|
+
request: ZuploRequest,
|
|
145
|
+
context: ZuploContext,
|
|
146
|
+
) {
|
|
147
|
+
let message: Record<string, unknown>;
|
|
148
|
+
try {
|
|
149
|
+
message = JSON.parse(data);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
context.log.warn("Dropping non-JSON WebSocket message");
|
|
152
|
+
return undefined; // drop messages that aren't valid JSON
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
delete message.ssn;
|
|
156
|
+
|
|
157
|
+
return JSON.stringify(message);
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Example: filter outbound messages
|
|
162
|
+
|
|
163
|
+
This outbound policy inspects messages from the backend and drops internal
|
|
164
|
+
events so they never reach the client. Other messages pass through unchanged.
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import { ZuploContext, ZuploRequest } from "@zuplo/runtime";
|
|
168
|
+
|
|
169
|
+
export async function filterOutbound(
|
|
170
|
+
data: string,
|
|
171
|
+
target: WebSocket,
|
|
172
|
+
source: WebSocket,
|
|
173
|
+
request: ZuploRequest,
|
|
174
|
+
context: ZuploContext,
|
|
175
|
+
) {
|
|
176
|
+
const message = JSON.parse(data);
|
|
177
|
+
|
|
178
|
+
if (message.type === "internal") {
|
|
179
|
+
return undefined; // drop internal events; the client never sees them
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return data;
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
You can configure multiple policies in each direction to compose behavior — for
|
|
187
|
+
example, one policy to validate a message against a schema and a second to
|
|
188
|
+
redact fields. Because each policy receives the previous policy's output, order
|
|
189
|
+
matters.
|
|
@@ -10,9 +10,8 @@ description: |
|
|
|
10
10
|
|
|
11
11
|
:::note{title="Beta"}
|
|
12
12
|
|
|
13
|
-
Cross App Access support is in beta and
|
|
14
|
-
|
|
15
|
-
model and policy options may change before general availability.
|
|
13
|
+
Cross App Access support is in beta. The configuration model and policy options
|
|
14
|
+
may change before general availability.
|
|
16
15
|
|
|
17
16
|
:::
|
|
18
17
|
|
|
@@ -7,15 +7,6 @@ description:
|
|
|
7
7
|
analytics.
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
:::note{title="Beta"}
|
|
11
|
-
|
|
12
|
-
The MCP Gateway is in beta. The configuration model and APIs may change before
|
|
13
|
-
general availability. Build with it, share feedback — just pin a
|
|
14
|
-
[compatibility date](./code-config/compatibility-dates.mdx) and expect the
|
|
15
|
-
occasional rough edge.
|
|
16
|
-
|
|
17
|
-
:::
|
|
18
|
-
|
|
19
10
|
The Zuplo MCP Gateway fronts one or more remote
|
|
20
11
|
[Model Context Protocol](https://modelcontextprotocol.io) (MCP) servers with a
|
|
21
12
|
single, OAuth-protected endpoint that AI clients connect to. Users sign in once
|
package/docs/policies/_index.md
CHANGED
|
@@ -43,7 +43,8 @@
|
|
|
43
43
|
| formdata-to-json-inbound | Form Data to JSON | Converts form data in the incoming request to JSON. | api-gateway |
|
|
44
44
|
| galileo-tracing-inbound | Galileo Tracing | Galileo Tracing Inbound Policy | ai-gateway |
|
|
45
45
|
| geo-filter-inbound | Geo-location filtering | Block requests based on geo-location parameters: country, region code, and ASN | api-gateway |
|
|
46
|
-
| graphql-analytics-outbound | GraphQL Analytics | Reports GraphQL errors returned in response bodies to Zuplo's GraphQL analytics. GraphQL servers following the standard Apollo / graphql-yoga pattern return `200 OK` with an `errors[]` array in the body when an operation fails, which HTTP-level analytics alone report as a success — add this policy to a GraphQL route and failed operations show up as failures on the GraphQL dashboard, classified by error type. Each error in `errors[]` is classified from its `extensions.code` following the Apollo Server conventions (`GRAPHQL_PARSE_FAILED` → `syntax`, `GRAPHQL_VALIDATION_FAILED` → `validation`, `UNAUTHENTICATED` / `FORBIDDEN` → `auth`, timeout codes → `timeout`); custom codes can be mapped with `errorCodeClassification`, and anything unrecognized falls back to `defaultErrorClass` (`resolver`). Optionally set `logErrors` to also write a structured warning per errored response.
|
|
46
|
+
| graphql-analytics-outbound | GraphQL Analytics | Reports GraphQL errors returned in response bodies to Zuplo's GraphQL analytics. GraphQL servers following the standard Apollo / graphql-yoga pattern return `200 OK` with an `errors[]` array in the body when an operation fails, which HTTP-level analytics alone report as a success — add this policy to a GraphQL route and failed operations show up as failures on the GraphQL dashboard, classified by error type. Each error in `errors[]` is classified from its `extensions.code` following the Apollo Server conventions (`GRAPHQL_PARSE_FAILED` → `syntax`, `GRAPHQL_VALIDATION_FAILED` → `validation`, `UNAUTHENTICATED` / `FORBIDDEN` → `auth`, timeout codes → `timeout`); custom codes can be mapped with `errorCodeClassification`, and anything unrecognized falls back to `defaultErrorClass` (`resolver`). Optionally set `logErrors` to also write a structured warning per errored response. The policy reads up to `maxScanBytes` of the body (128 KiB by default, 5 MiB maximum), scanning it for the `errors` token; a response larger than that is treated as error-free. When the token is found and the body fits, it is parsed and its errors reported. The response always passes through unchanged — the body is read from a clone, and any internal failure is swallowed so reporting can never break the request. The route must be marked `x-graphql: true` in `routes.oas.json` (which enables GraphQL analytics for the route); without the marker the policy logs a warning and does nothing. | api-gateway |
|
|
47
|
+
| graphql-cache-inbound | GraphQL Cache | Caches GraphQL query responses at the edge so identical queries are served without a round-trip to the origin. Unlike CDN caching that keys on the raw request body, this policy parses the GraphQL document and normalizes it before hashing: insignificant whitespace, field formatting, and fragment layout are collapsed, and variable object keys are sorted. Two requests that are semantically identical therefore share a cache entry even when their bodies differ byte-for-byte. There is no query size or nesting-depth limit. Only `query` operations are cached. Mutations, subscriptions, malformed documents, and non-JSON bodies are forwarded to the origin untouched. Cache hits and misses are reported on the `x-cache` response header, with a short key fingerprint on `x-cache-key`. To avoid serving one user's data to another, requests carrying an `authorization` or `cookie` header are not cached by default. Use `cacheKeyHeaders` to opt into caching them: each listed header's value is included in the cache key, so distinct credentials get distinct cache entries. | api-gateway |
|
|
47
48
|
| graphql-complexity-limit-inbound | GraphQL Complexity Limit | Policy that limits the complexity and depth of GraphQL queries to prevent abuse. Protects your GraphQL API from expensive queries that could cause performance issues or denial of service attacks. | api-gateway |
|
|
48
49
|
| graphql-disable-introspection-inbound | GraphQL Disable Introspection | Policy that disables GraphQL introspection queries in production. Introspection allows clients to discover the schema, which can be a security risk as it exposes your entire API structure. | api-gateway |
|
|
49
50
|
| graphql-introspection-filter-outbound | GraphQL Introspection Filter | Filters GraphQL introspection responses to exclude specific types and fields. This policy intercepts GraphQL introspection query responses and removes configured types and fields from the schema. Useful for hiding internal types or sensitive fields from the public schema. | api-gateway |
|
|
@@ -99,6 +100,7 @@
|
|
|
99
100
|
| sleep-inbound | Sleep / Delay | Add a delay to the incoming request. Useful for testing. | api-gateway |
|
|
100
101
|
| stripe-webhook-verification-inbound | Stripe Webhook Auth | The Stripe Webhook policy validates the authenticity of an incoming Stripe webhook. | api-gateway |
|
|
101
102
|
| supabase-jwt-auth-inbound | Supabase JWT Auth | The Supabase JWT Authentication policy supports user JWT tokens created by Supabase. | api-gateway |
|
|
103
|
+
| traffic-splitting-inbound | Traffic Splitting | Splits traffic randomly across a set of weighted base paths. On each request one base path is selected (weighted by `weight`) and written to the request custom context at `customOutputProperty`. Reference it from a later URL Rewrite `rewritePattern` or URL Forward `baseUrl`, e.g. `${context.custom.trafficSplitting.basePath}`. | api-gateway |
|
|
102
104
|
| transform-body-inbound | Transform Request Body | Transform the body of an incoming request. | api-gateway |
|
|
103
105
|
| transform-body-outbound | Transform Response Body | Transform the body of an outgoing response. | api-gateway |
|
|
104
106
|
| upstream-azure-ad-service-auth-inbound | Upstream Azure AD Service Auth | Uses Azure Active Directory to add an Authorization header to the request in order to authenticate requests using Azure identity. | api-gateway |
|
|
@@ -41,10 +41,13 @@ result in the batch.
|
|
|
41
41
|
## What is inspected
|
|
42
42
|
|
|
43
43
|
Only responses with a JSON content type (`application/json` or any `+json` type
|
|
44
|
-
such as `application/graphql-response+json`) are read
|
|
45
|
-
`
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
such as `application/graphql-response+json`) are read. The policy reads up to
|
|
45
|
+
`maxScanBytes` of the body (128 KiB by default, 5 MiB maximum) and scans it for
|
|
46
|
+
the `errors` token; a response larger than that — by `Content-Length` when the
|
|
47
|
+
header is present, or measured while reading when it is absent — is treated as
|
|
48
|
+
error-free, so any errors it carries go unreported. Raise `maxScanBytes` if your
|
|
49
|
+
GraphQL responses are larger. Everything else passes through without the body
|
|
50
|
+
being touched.
|
|
48
51
|
|
|
49
52
|
## Logging
|
|
50
53
|
|
|
@@ -63,8 +66,9 @@ alert on it.
|
|
|
63
66
|
`resolver`
|
|
64
67
|
- `logErrors`: Also log a structured warning per errored response. **Default:**
|
|
65
68
|
`false`
|
|
66
|
-
- `
|
|
67
|
-
|
|
69
|
+
- `maxScanBytes`: How many bytes of the response body to read and scan for the
|
|
70
|
+
`errors` token. A larger response is treated as error-free. Capped at 5 MiB.
|
|
71
|
+
**Default:** `131072` (128 KiB)
|
|
68
72
|
|
|
69
73
|
## Usage
|
|
70
74
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"isHidden": false,
|
|
12
12
|
"requiresAI": false,
|
|
13
13
|
"products": ["api-gateway"],
|
|
14
|
-
"description": "Reports GraphQL errors returned in response bodies to Zuplo's GraphQL analytics. GraphQL servers following the standard Apollo / graphql-yoga pattern return `200 OK` with an `errors[]` array in the body when an operation fails, which HTTP-level analytics alone report as a success — add this policy to a GraphQL route and failed operations show up as failures on the GraphQL dashboard, classified by error type.\n\nEach error in `errors[]` is classified from its `extensions.code` following the Apollo Server conventions (`GRAPHQL_PARSE_FAILED` → `syntax`, `GRAPHQL_VALIDATION_FAILED` → `validation`, `UNAUTHENTICATED` / `FORBIDDEN` → `auth`, timeout codes → `timeout`); custom codes can be mapped with `errorCodeClassification`, and anything unrecognized falls back to `defaultErrorClass` (`resolver`). Optionally set `logErrors` to also write a structured warning per errored response.
|
|
14
|
+
"description": "Reports GraphQL errors returned in response bodies to Zuplo's GraphQL analytics. GraphQL servers following the standard Apollo / graphql-yoga pattern return `200 OK` with an `errors[]` array in the body when an operation fails, which HTTP-level analytics alone report as a success — add this policy to a GraphQL route and failed operations show up as failures on the GraphQL dashboard, classified by error type.\n\nEach error in `errors[]` is classified from its `extensions.code` following the Apollo Server conventions (`GRAPHQL_PARSE_FAILED` → `syntax`, `GRAPHQL_VALIDATION_FAILED` → `validation`, `UNAUTHENTICATED` / `FORBIDDEN` → `auth`, timeout codes → `timeout`); custom codes can be mapped with `errorCodeClassification`, and anything unrecognized falls back to `defaultErrorClass` (`resolver`). Optionally set `logErrors` to also write a structured warning per errored response. The policy reads up to `maxScanBytes` of the body (128 KiB by default, 5 MiB maximum), scanning it for the `errors` token; a response larger than that is treated as error-free. When the token is found and the body fits, it is parsed and its errors reported.\n\nThe response always passes through unchanged — the body is read from a clone, and any internal failure is swallowed so reporting can never break the request. The route must be marked `x-graphql: true` in `routes.oas.json` (which enables GraphQL analytics for the route); without the marker the policy logs a warning and does nothing.",
|
|
15
15
|
"deprecatedMessage": "",
|
|
16
16
|
"required": ["handler"],
|
|
17
17
|
"properties": {
|
|
@@ -64,12 +64,13 @@
|
|
|
64
64
|
"default": false,
|
|
65
65
|
"description": "When `true`, also write a structured warning to the request log (message, `extensions.code`, and path of each error — capped at the first 10) whenever a response contains GraphQL errors."
|
|
66
66
|
},
|
|
67
|
-
"
|
|
67
|
+
"maxScanBytes": {
|
|
68
68
|
"type": "integer",
|
|
69
69
|
"minimum": 1,
|
|
70
|
-
"
|
|
70
|
+
"maximum": 5242880,
|
|
71
|
+
"default": 131072,
|
|
71
72
|
"x-show-example": false,
|
|
72
|
-
"description": "
|
|
73
|
+
"description": "How many bytes of the response body the policy reads to look for GraphQL errors. The body is read and scanned for the `errors` token up to this limit; a body larger than the limit — by `Content-Length`, or measured while reading when the header is absent — is treated as error-free, so any errors it carries go unreported. The default of 128 KiB suits the common case, where servers emit `errors` near the front of the body. Raise it (up to the 5 MiB maximum) to detect errors in larger responses, at the cost of reading more of every response. When the token is found and the body fits within the limit, the body is parsed and its errors are reported."
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
76
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
The GraphQL Cache policy stores successful GraphQL query responses in a
|
|
2
|
+
[ZoneCache](https://zuplo.com/docs/articles/zonecache) and serves later,
|
|
3
|
+
identical queries directly from the edge.
|
|
4
|
+
|
|
5
|
+
### How caching works
|
|
6
|
+
|
|
7
|
+
For every inbound request the policy:
|
|
8
|
+
|
|
9
|
+
1. Reads the request body and parses it as GraphQL JSON
|
|
10
|
+
(`{ "query": "...", "variables": { ... }, "operationName": "..." }`).
|
|
11
|
+
2. Parses the query and re-prints it, producing a canonical form that ignores
|
|
12
|
+
insignificant whitespace, field formatting, and fragment layout.
|
|
13
|
+
3. Canonicalizes the `variables` by recursively sorting object keys.
|
|
14
|
+
4. Hashes the normalized query, canonicalized variables, and `operationName`
|
|
15
|
+
(SHA-256) into a cache key. `operationName` is included because a document
|
|
16
|
+
with multiple operations returns a different response depending on which
|
|
17
|
+
operation the client selects.
|
|
18
|
+
|
|
19
|
+
On a **hit**, the cached response is returned immediately. On a **miss**, the
|
|
20
|
+
request is forwarded to the origin and a successful response is stored for
|
|
21
|
+
future requests.
|
|
22
|
+
|
|
23
|
+
Every response served or stored by the policy carries two headers:
|
|
24
|
+
|
|
25
|
+
- **`x-cache`** — `HIT` when served from cache, `MISS` when fetched from the
|
|
26
|
+
origin.
|
|
27
|
+
- **`x-cache-key`** — the first 8 characters of the cache key, useful for
|
|
28
|
+
confirming that two requests resolve to the same entry.
|
|
29
|
+
|
|
30
|
+
Both headers are added to `access-control-expose-headers` so browsers can read
|
|
31
|
+
them, without overwriting any value an upstream CORS policy already set.
|
|
32
|
+
|
|
33
|
+
### What is and isn't cached
|
|
34
|
+
|
|
35
|
+
- Only `query` operations are cached. **Mutations** and **subscriptions** are
|
|
36
|
+
forwarded to the origin and never cached. In a multi-operation document the
|
|
37
|
+
operation selected by `operationName` is the one that decides this.
|
|
38
|
+
- **Malformed** GraphQL and **non-JSON** bodies are forwarded untouched so the
|
|
39
|
+
origin can return a proper error. Documents with multiple operations but no
|
|
40
|
+
`operationName` (or an `operationName` that matches none) are also forwarded.
|
|
41
|
+
- Only `200` responses are cached, and only when the body is a successful
|
|
42
|
+
GraphQL result. Because GraphQL returns execution errors with a `200` status
|
|
43
|
+
and an `errors` array, responses carrying a non-empty `errors` array — and
|
|
44
|
+
non-JSON `200` bodies — are **not** cached.
|
|
45
|
+
|
|
46
|
+
### Options
|
|
47
|
+
|
|
48
|
+
- **`cacheName`** - The name of the cache used to store responses. Defaults to
|
|
49
|
+
`graphql-responses`. Routes that share a name share a cache; use distinct
|
|
50
|
+
names to isolate caches per route or per upstream.
|
|
51
|
+
- **`ttlSeconds`** - How long, in seconds, a cached response is served before it
|
|
52
|
+
is considered stale and the next request is forwarded to the origin to refresh
|
|
53
|
+
the entry. Defaults to `60`.
|
|
54
|
+
- **`cacheKeyHeaders`** - Request header names whose values are included in the
|
|
55
|
+
cache key (matched case-insensitively), and the control for how credentialed
|
|
56
|
+
requests are cached. See
|
|
57
|
+
[Authentication and per-user caching](#authentication-and-per-user-caching)
|
|
58
|
+
below. Defaults to omitted.
|
|
59
|
+
|
|
60
|
+
### Authentication and per-user caching
|
|
61
|
+
|
|
62
|
+
A response cache keyed only on the query would serve the first user's response
|
|
63
|
+
to everyone. To prevent that, the policy **does not cache requests that carry an
|
|
64
|
+
`authorization` or `cookie` header** by default — those requests are forwarded
|
|
65
|
+
to the origin every time.
|
|
66
|
+
|
|
67
|
+
To cache authenticated traffic safely, list the headers that make a response
|
|
68
|
+
user-specific in `cacheKeyHeaders`. Each listed header's value becomes part of
|
|
69
|
+
the cache key, so every distinct value (for example, every bearer token) gets
|
|
70
|
+
its own cache entry:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"name": "graphql-cache",
|
|
75
|
+
"policyType": "graphql-cache-inbound",
|
|
76
|
+
"handler": {
|
|
77
|
+
"export": "GraphQLCacheInboundPolicy",
|
|
78
|
+
"module": "$import(@zuplo/graphql)",
|
|
79
|
+
"options": {
|
|
80
|
+
"ttlSeconds": 30,
|
|
81
|
+
"cacheKeyHeaders": ["authorization"]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
If a request still carries an `authorization` or `cookie` header that is **not**
|
|
88
|
+
in `cacheKeyHeaders`, it is left uncached — so partially configuring the
|
|
89
|
+
allowlist fails safe rather than leaking across users.
|
|
90
|
+
|
|
91
|
+
#### Caching credentialed requests as a single shared entry
|
|
92
|
+
|
|
93
|
+
Sometimes a request must carry `authorization` (or `cookie`) to be authorized,
|
|
94
|
+
but the response is identical for everyone allowed through — the credential
|
|
95
|
+
gates access without changing the data. In that case, set `cacheKeyHeaders` to
|
|
96
|
+
an **empty array** to cache one response and share it across all callers:
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"name": "graphql-cache",
|
|
101
|
+
"policyType": "graphql-cache-inbound",
|
|
102
|
+
"handler": {
|
|
103
|
+
"export": "GraphQLCacheInboundPolicy",
|
|
104
|
+
"module": "$import(@zuplo/graphql)",
|
|
105
|
+
"options": {
|
|
106
|
+
"cacheKeyHeaders": []
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
This is distinct from omitting the option: omitting it keeps the safe default
|
|
113
|
+
(credentialed requests are not cached), whereas `[]` is an explicit assertion
|
|
114
|
+
that the response does not depend on the caller. **Only use `[]` when that is
|
|
115
|
+
true** — otherwise one caller's response will be served to others.
|
|
116
|
+
|
|
117
|
+
Response cookies are never shared. `Set-Cookie` (along with `Set-Cookie2` and
|
|
118
|
+
`Clear-Site-Data`) is stripped from the stored entry, so a cookie an origin sets
|
|
119
|
+
on one caller's response is never replayed to another from cache. The caller
|
|
120
|
+
whose request reached the origin still receives the original `Set-Cookie`.
|
|
121
|
+
|
|
122
|
+
### Example
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"name": "graphql-cache",
|
|
127
|
+
"policyType": "graphql-cache-inbound",
|
|
128
|
+
"handler": {
|
|
129
|
+
"export": "GraphQLCacheInboundPolicy",
|
|
130
|
+
"module": "$import(@zuplo/graphql)",
|
|
131
|
+
"options": {
|
|
132
|
+
"cacheName": "graphql-responses",
|
|
133
|
+
"ttlSeconds": 60
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
This policy caches GraphQL query responses at the edge so identical queries are
|
|
2
|
+
served without a round-trip to your origin.
|
|
3
|
+
|
|
4
|
+
Unlike CDN caching that keys on the raw request body, this policy parses each
|
|
5
|
+
GraphQL document and normalizes it before building a cache key. Insignificant
|
|
6
|
+
whitespace, field formatting, and fragment layout are collapsed, and variable
|
|
7
|
+
object keys are sorted. As a result, two requests that are semantically
|
|
8
|
+
identical share a cache entry even when their bodies differ byte-for-byte — and
|
|
9
|
+
there is no query size or nesting-depth limit.
|
|
10
|
+
|
|
11
|
+
Only `query` operations are cached. Mutations, subscriptions, malformed
|
|
12
|
+
documents, and non-GraphQL bodies are always forwarded to the origin untouched.
|
|
13
|
+
To avoid serving one user's data to another, requests carrying an
|
|
14
|
+
`authorization` or `cookie` header are not cached unless you opt in with the
|
|
15
|
+
`cacheKeyHeaders` option.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft-07/schema",
|
|
3
|
+
"$id": "https://cdn.zuplo.com/policies/graphql/schemas/graphql-cache-inbound.json",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"title": "GraphQL Cache",
|
|
6
|
+
"isDeprecated": false,
|
|
7
|
+
"isPaidAddOn": false,
|
|
8
|
+
"isEnterprise": false,
|
|
9
|
+
"isInternal": false,
|
|
10
|
+
"isBeta": false,
|
|
11
|
+
"isHidden": false,
|
|
12
|
+
"requiresAI": false,
|
|
13
|
+
"products": ["api-gateway"],
|
|
14
|
+
"description": "Caches GraphQL query responses at the edge so identical queries are served without a round-trip to the origin.\n\nUnlike CDN caching that keys on the raw request body, this policy parses the GraphQL document and normalizes it before hashing: insignificant whitespace, field formatting, and fragment layout are collapsed, and variable object keys are sorted. Two requests that are semantically identical therefore share a cache entry even when their bodies differ byte-for-byte. There is no query size or nesting-depth limit.\n\nOnly `query` operations are cached. Mutations, subscriptions, malformed documents, and non-JSON bodies are forwarded to the origin untouched. Cache hits and misses are reported on the `x-cache` response header, with a short key fingerprint on `x-cache-key`.\n\nTo avoid serving one user's data to another, requests carrying an `authorization` or `cookie` header are not cached by default. Use `cacheKeyHeaders` to opt into caching them: each listed header's value is included in the cache key, so distinct credentials get distinct cache entries.",
|
|
15
|
+
"deprecatedMessage": "",
|
|
16
|
+
"required": ["handler"],
|
|
17
|
+
"properties": {
|
|
18
|
+
"handler": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"default": {},
|
|
21
|
+
"required": ["export", "module", "options"],
|
|
22
|
+
"properties": {
|
|
23
|
+
"export": {
|
|
24
|
+
"const": "GraphQLCacheInboundPolicy",
|
|
25
|
+
"description": "The name of the exported type"
|
|
26
|
+
},
|
|
27
|
+
"module": {
|
|
28
|
+
"const": "$import(@zuplo/graphql)",
|
|
29
|
+
"description": "The module containing the policy"
|
|
30
|
+
},
|
|
31
|
+
"options": {
|
|
32
|
+
"title": "GraphQLCacheInboundPolicyOptions",
|
|
33
|
+
"type": "object",
|
|
34
|
+
"description": "The options for this policy.",
|
|
35
|
+
"required": [],
|
|
36
|
+
"additionalProperties": false,
|
|
37
|
+
"properties": {
|
|
38
|
+
"cacheName": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"default": "graphql-responses",
|
|
41
|
+
"examples": ["graphql-responses"],
|
|
42
|
+
"description": "The name of the cache used to store responses. Routes that share a name share a cache; use distinct names to isolate caches per route or per upstream."
|
|
43
|
+
},
|
|
44
|
+
"ttlSeconds": {
|
|
45
|
+
"type": "number",
|
|
46
|
+
"default": 60,
|
|
47
|
+
"examples": [60],
|
|
48
|
+
"description": "How long, in seconds, a cached response is served before it is considered stale and the next request is forwarded to the origin to refresh the entry."
|
|
49
|
+
},
|
|
50
|
+
"cacheKeyHeaders": {
|
|
51
|
+
"type": "array",
|
|
52
|
+
"items": {
|
|
53
|
+
"type": "string"
|
|
54
|
+
},
|
|
55
|
+
"examples": [["authorization"]],
|
|
56
|
+
"description": "Request header names whose values are included in the cache key (matched case-insensitively), and the control for how credentialed requests are cached. Omit this option (the default) and requests carrying an `authorization` or `cookie` header are not cached, to avoid serving one user's response to another. List those headers to cache such requests keyed per value, so each distinct value gets its own entry (a credential header you don't list still blocks caching, so a partial list fails safe). Set it to an empty array `[]` to cache a single response shared across all callers — only do this when the response does not depend on who is calling, as it disables the per-user safety check."
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"examples": [
|
|
62
|
+
{
|
|
63
|
+
"export": "GraphQLCacheInboundPolicy",
|
|
64
|
+
"module": "$import(@zuplo/graphql)",
|
|
65
|
+
"options": {
|
|
66
|
+
"cacheKeyHeaders": ["authorization"],
|
|
67
|
+
"cacheName": "graphql-responses",
|
|
68
|
+
"ttlSeconds": 60
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|