zuplo 6.71.10 → 6.71.11

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.
@@ -88,12 +88,26 @@ zuplo ca-certificate list --account your-account
88
88
  See the [`ca-certificate` CLI reference](../cli/ca-certificate-create.mdx) for
89
89
  all available subcommands (`create`, `list`, `describe`, `update`, `delete`).
90
90
 
91
- :::tip{title="Using an intermediate CA"}
91
+ :::caution{title="Upload the self-signed root CA, not an intermediate"}
92
92
 
93
- If your client certificates are issued by an intermediate CA (rather than
94
- directly by your root), upload the root CA certificate that anchors the chain.
95
- Clients must send the leaf certificate plus any intermediate certificates when
96
- they connect.
93
+ Zuplo must build a complete chain from the presented client certificate up to a
94
+ trust anchor. Upload the **self-signed root** CA that anchors the chain — not an
95
+ intermediate or subordinate CA. Uploading a subordinate CA is the most common
96
+ cause of the `FAILED to get issuer certificate` error (see
97
+ [Troubleshooting](#failed-to-get-issuer-certificate)).
98
+
99
+ To confirm a certificate is a self-signed root, check that its subject and
100
+ issuer are identical:
101
+
102
+ ```bash
103
+ openssl x509 -in ca.pem -noout -subject -issuer -nameopt RFC2253
104
+ ```
105
+
106
+ If `subject` and `issuer` match, it's a root. If they differ, the file is an
107
+ intermediate CA — trace the chain to the root and upload that instead. When your
108
+ client certificates are issued by an intermediate CA, clients still send the
109
+ leaf certificate plus any intermediates when they connect (see
110
+ [Test with curl](#4-test-with-curl)).
97
111
 
98
112
  :::
99
113
 
@@ -315,11 +329,73 @@ the policy in a working-copy or preview environment.
315
329
  `context.incomingRequestProperties.clientMtlsVerificationReason` to see why
316
330
  verification failed.
317
331
 
332
+ ### `FAILED to get issuer certificate`
333
+
334
+ This error means Zuplo can't build a complete chain from the presented client
335
+ certificate up to a trusted root. The usual cause is uploading an **intermediate
336
+ or subordinate CA** instead of the **self-signed root** CA that anchors the
337
+ chain.
338
+
339
+ Confirm what you uploaded. A self-signed root has an identical subject and
340
+ issuer:
341
+
342
+ ```bash
343
+ openssl x509 -in ca.pem -noout -subject -issuer -nameopt RFC2253
344
+ ```
345
+
346
+ If `subject` and `issuer` differ, the file is an intermediate CA. Find the root
347
+ that anchors the chain and re-upload it:
348
+
349
+ ```bash
350
+ # Remove the incorrect CA
351
+ zuplo ca-certificate delete --cert-id mtlsca_abc123 --account your-account
352
+
353
+ # Upload the self-signed root instead
354
+ zuplo ca-certificate create --name my_ca --cert ./root-ca.pem --account your-account
355
+ ```
356
+
357
+ If your CA is an Active Directory Certificate Services (AD CS) deployment,
358
+ export the issuing CA's own certificate and inspect it:
359
+
360
+ ```bash
361
+ # Export the CA certificate (often DER-encoded)
362
+ certutil -ca.cert ca.cer
363
+
364
+ # Convert DER to the PEM format Zuplo requires
365
+ openssl x509 -inform der -in ca.cer -out ca.pem
366
+
367
+ # Verify subject == issuer (a root); if not, export the root above it instead
368
+ openssl x509 -in ca.pem -noout -subject -issuer -nameopt RFC2253
369
+ ```
370
+
371
+ ### `client certificate metadata not provided`
372
+
373
+ The certificate verified at the edge, but the parsed certificate wasn't
374
+ forwarded to your gateway workers, so `request.user.data.mtlsAuth` is empty even
375
+ though the request was authenticated.
376
+
377
+ The most common cause is an **oversized leaf client certificate**. Zuplo's edge
378
+ can only forward client certificates up to roughly 10 KB of DER-encoded data.
379
+ Certificates with large RSA keys, many Subject Alternative Names, or large
380
+ custom extensions can exceed this. Check the DER size of the leaf certificate:
381
+
382
+ ```bash
383
+ openssl x509 -in client.pem -outform der | wc -c
384
+ ```
385
+
386
+ If the result is near or above ~10,000 bytes, reissue a smaller leaf
387
+ certificate. Trim unnecessary extensions and SANs, or switch to ECDSA keys
388
+ instead of large (4096-bit) RSA keys. This limit applies to the **leaf**
389
+ certificate the edge forwards, not the full chain.
390
+
318
391
  ### `request.user.data.mtlsAuth` is missing
319
392
 
320
393
  - The policy only attaches metadata when a parseable client certificate is
321
394
  present on the request. Confirm the client is sending one.
322
395
  - Verify the route includes the `mtls-auth-inbound` policy.
396
+ - If the certificate verifies but metadata is still missing, check the leaf
397
+ certificate size (see
398
+ [`client certificate metadata not provided`](#client-certificate-metadata-not-provided)).
323
399
 
324
400
  ### Custom domains
325
401
 
@@ -331,6 +407,27 @@ that custom domain.
331
407
  If you add a custom domain later and your clients aren't being verified against
332
408
  it, contact [support@zuplo.com](mailto:support@zuplo.com).
333
409
 
410
+ ### Custom domains behind your own CDN
411
+
412
+ Inbound mTLS requires the TLS handshake to terminate at Zuplo's edge, because
413
+ that's where the client certificate is verified and parsed. If you front your
414
+ Zuplo gateway with **your own CDN** (for example, your own Cloudflare zone) that
415
+ terminates TLS before traffic reaches Zuplo, the handshake — and the client
416
+ certificate — ends at your CDN. Zuplo never sees the certificate, so the
417
+ `mtls-auth-inbound` policy has nothing to verify.
418
+
419
+ You have two supported options:
420
+
421
+ - **Let Zuplo terminate TLS.** Point clients at a Zuplo-managed gateway domain,
422
+ or configure your custom domain directly on Zuplo so Zuplo terminates TLS. The
423
+ client certificate then reaches Zuplo's edge and inbound mTLS works as
424
+ documented above.
425
+ - **Verify mTLS at your CDN.** If you must keep your own CDN in front, terminate
426
+ and verify the client certificate at the CDN, then forward the verified
427
+ identity to Zuplo in a request header. Validate that header in a Zuplo policy
428
+ instead of relying on `mtls-auth-inbound`. Make sure the header can't be
429
+ spoofed by clients that bypass your CDN.
430
+
334
431
  ## Additional resources
335
432
 
336
433
  - [`mtls-auth-inbound` policy reference](../policies/mtls-auth-inbound.mdx)
@@ -0,0 +1,307 @@
1
+ ---
2
+ title: Transform Route Parameters for URL Rewrite
3
+ sidebar_label: Transform Route Parameters
4
+ description:
5
+ Learn how to use an inbound policy to transform route parameter values before
6
+ the URL Rewrite handler forwards the request to your backend.
7
+ tags:
8
+ - custom-code
9
+ - backends
10
+ ---
11
+
12
+ This guide explains how to transform incoming route parameter values in an
13
+ inbound policy before the [URL Rewrite handler](../handlers/url-rewrite.mdx)
14
+ uses them to build the upstream URL. This pattern is useful when your public API
15
+ paths use different naming conventions than your internal backend.
16
+
17
+ ## Overview
18
+
19
+ When you use the URL Rewrite handler, it builds the upstream URL by
20
+ interpolating values like `${params.resourceType}` directly from the incoming
21
+ route parameters. Sometimes, however, you need to **change** those values before
22
+ the rewrite happens. Common scenarios include:
23
+
24
+ - **Value mapping** — translating a public-facing parameter like `order` to an
25
+ internal value like `customerorder`
26
+ - **Case normalization** — converting `Products` to `products` before forwarding
27
+ - **Path translation** — mapping user-friendly slugs to internal identifiers
28
+
29
+ The recommended approach is to read the route parameters in an inbound policy,
30
+ transform them, store the results on
31
+ [`context.custom`](../programmable-api/zuplo-context.mdx), and reference the
32
+ transformed values in the URL Rewrite pattern.
33
+
34
+ ## Step-by-Step Example
35
+
36
+ The solution has three parts: an **inbound policy** that reads `request.params`
37
+ and stores transformed values on `context.custom`, a **URL Rewrite handler**
38
+ that references those values using `${context.custom.*}` in the
39
+ `rewritePattern`, and **route configuration** that wires the two together.
40
+
41
+ Imagine your public API exposes a route like `/api/:resourceType/:resourceId`,
42
+ but your backend expects the resource type to be prefixed with `customer`. A
43
+ request to `/api/order/123` should be forwarded to
44
+ `https://backend.example.com/api/customerorder/123`.
45
+
46
+ ### 1. Write the Inbound Policy
47
+
48
+ Create a custom inbound policy that reads the route parameters, transforms the
49
+ values, and stores them on `context.custom`:
50
+
51
+ ```ts title="modules/transform-params.ts"
52
+ import { ZuploContext, ZuploRequest } from "@zuplo/runtime";
53
+
54
+ export default async function (
55
+ request: ZuploRequest,
56
+ context: ZuploContext,
57
+ options: any,
58
+ policyName: string,
59
+ ): Promise<ZuploRequest | Response> {
60
+ // Read the original route parameter
61
+ const resourceType = request.params.resourceType;
62
+
63
+ // Transform the value — prefix with "customer"
64
+ const transformedResourceType = `customer${resourceType}`;
65
+
66
+ // Store the transformed value on context.custom
67
+ context.custom.transformedResourceType = transformedResourceType;
68
+
69
+ context.log.info({
70
+ message: "Transformed route parameter",
71
+ original: resourceType,
72
+ transformed: transformedResourceType,
73
+ });
74
+
75
+ return request;
76
+ }
77
+ ```
78
+
79
+ ### 2. Register the Policy
80
+
81
+ Add the policy to `config/policies.json`:
82
+
83
+ ```json title="config/policies.json"
84
+ {
85
+ "policies": [
86
+ {
87
+ "name": "transform-params",
88
+ "policyType": "custom-code-inbound",
89
+ "handler": {
90
+ "export": "default",
91
+ "module": "$import(./modules/transform-params)"
92
+ }
93
+ }
94
+ ]
95
+ }
96
+ ```
97
+
98
+ ### 3. Configure the Route
99
+
100
+ Define the route in `config/routes.oas.json` with the inbound policy and a URL
101
+ Rewrite handler that references `context.custom`:
102
+
103
+ ```json title="config/routes.oas.json"
104
+ {
105
+ "paths": {
106
+ "/api/{resourceType}/{resourceId}": {
107
+ "x-zuplo-path": {
108
+ "pathMode": "open-api"
109
+ },
110
+ "get": {
111
+ "summary": "Get resource by type and ID",
112
+ "x-zuplo-route": {
113
+ "corsPolicy": "none",
114
+ "handler": {
115
+ "export": "urlRewriteHandler",
116
+ "module": "$import(@zuplo/runtime)",
117
+ "options": {
118
+ "rewritePattern": "https://backend.example.com/api/${context.custom.transformedResourceType}/${params.resourceId}"
119
+ }
120
+ },
121
+ "policies": {
122
+ "inbound": ["transform-params"]
123
+ }
124
+ }
125
+ }
126
+ }
127
+ }
128
+ }
129
+ ```
130
+
131
+ With this configuration, a request to `/api/order/123` flows through the
132
+ pipeline as follows:
133
+
134
+ 1. The route matches with `params.resourceType = "order"` and
135
+ `params.resourceId = "123"`
136
+ 2. The `transform-params` inbound policy runs and sets
137
+ `context.custom.transformedResourceType = "customerorder"`
138
+ 3. The URL Rewrite handler builds the upstream URL:
139
+ `https://backend.example.com/api/customerorder/123`
140
+
141
+ ## Common Pitfall: Modifying `request.params` Directly
142
+
143
+ :::caution
144
+
145
+ Do not try to transform route parameters by constructing a new `ZuploRequest`
146
+ with modified `params` and expecting the URL Rewrite handler to pick them up.
147
+
148
+ :::
149
+
150
+ A common first attempt is to create a new
151
+ [`ZuploRequest`](../programmable-api/zuplo-request.mdx) with different `params`:
152
+
153
+ ```ts
154
+ // ⚠️ This approach does NOT work as expected with URL Rewrite
155
+ const newRequest = new ZuploRequest(request, {
156
+ params: {
157
+ ...request.params,
158
+ resourceType: "customerorder",
159
+ },
160
+ });
161
+ return newRequest;
162
+ ```
163
+
164
+ In practice, the URL Rewrite handler evaluates `${params.*}` against the
165
+ route-level parameters rather than the request object returned by a policy. This
166
+ means the rewritten URL may contain `undefined` segments instead of your
167
+ transformed values. Use `context.custom` for reliable interpolation of
168
+ transformed values — the URL Rewrite handler's `rewritePattern` fully supports
169
+ `${context.custom.*}`, and values set in an inbound policy are available when
170
+ the handler runs.
171
+
172
+ ## Variations
173
+
174
+ ### Using a Lookup Map
175
+
176
+ For more complex mappings where the transformation is not a simple string
177
+ operation, use a lookup object:
178
+
179
+ ```ts title="modules/transform-params-map.ts"
180
+ import { ZuploContext, ZuploRequest, HttpProblems } from "@zuplo/runtime";
181
+
182
+ // Map public resource types to internal names
183
+ const RESOURCE_TYPE_MAP: Record<string, string> = {
184
+ order: "customerorder",
185
+ invoice: "billing-invoice",
186
+ profile: "user-profile",
187
+ subscription: "recurring-plan",
188
+ };
189
+
190
+ export default async function (
191
+ request: ZuploRequest,
192
+ context: ZuploContext,
193
+ options: any,
194
+ policyName: string,
195
+ ): Promise<ZuploRequest | Response> {
196
+ const resourceType = request.params.resourceType;
197
+ const mappedType = RESOURCE_TYPE_MAP[resourceType];
198
+
199
+ if (!mappedType) {
200
+ return HttpProblems.notFound(request, context, {
201
+ detail: `Unknown resource type: ${resourceType}`,
202
+ });
203
+ }
204
+
205
+ context.custom.transformedResourceType = mappedType;
206
+
207
+ return request;
208
+ }
209
+ ```
210
+
211
+ ### Transforming Multiple Parameters
212
+
213
+ You can transform any number of route parameters and store each on
214
+ `context.custom`. Reference them individually in the rewrite pattern:
215
+
216
+ ```ts title="modules/transform-multiple-params.ts"
217
+ import { ZuploContext, ZuploRequest } from "@zuplo/runtime";
218
+
219
+ export default async function (
220
+ request: ZuploRequest,
221
+ context: ZuploContext,
222
+ options: any,
223
+ policyName: string,
224
+ ): Promise<ZuploRequest | Response> {
225
+ // Normalize casing
226
+ context.custom.version = request.params.version?.toLowerCase();
227
+
228
+ // Map resource type
229
+ context.custom.resource =
230
+ request.params.resource === "users" ? "customers" : request.params.resource;
231
+
232
+ return request;
233
+ }
234
+ ```
235
+
236
+ Then use both values in the rewrite pattern:
237
+
238
+ ```json
239
+ {
240
+ "rewritePattern": "https://backend.example.com/${context.custom.version}/${context.custom.resource}/${params.id}"
241
+ }
242
+ ```
243
+
244
+ ### Combining with Body Transformation
245
+
246
+ If your API also needs to transform values in the request body alongside route
247
+ parameters, you can handle both in the same inbound policy. Create a new
248
+ `ZuploRequest` with a modified body while storing the route parameter
249
+ transformations on `context.custom`:
250
+
251
+ ```ts title="modules/transform-params-and-body.ts"
252
+ import { ZuploContext, ZuploRequest } from "@zuplo/runtime";
253
+
254
+ export default async function (
255
+ request: ZuploRequest,
256
+ context: ZuploContext,
257
+ options: any,
258
+ policyName: string,
259
+ ): Promise<ZuploRequest | Response> {
260
+ // Transform route parameter
261
+ context.custom.transformedResourceType = `customer${request.params.resourceType}`;
262
+
263
+ // Transform the request body if present
264
+ if (request.headers.get("content-type")?.includes("application/json")) {
265
+ const body = await request.json();
266
+
267
+ // Map fields in the body to match the backend schema
268
+ const transformedBody = {
269
+ ...body,
270
+ type: context.custom.transformedResourceType,
271
+ };
272
+
273
+ // Return a new request with the modified body
274
+ return new ZuploRequest(request, {
275
+ body: JSON.stringify(transformedBody),
276
+ });
277
+ }
278
+
279
+ return request;
280
+ }
281
+ ```
282
+
283
+ ## Best Practices
284
+
285
+ - **Use descriptive keys on `context.custom`** — names like
286
+ `context.custom.transformedResourceType` are easier to debug than generic keys
287
+ like `context.custom.value`
288
+ - **Log transformations** — use `context.log` to record original and transformed
289
+ values so you can trace issues in production
290
+ - **Validate before transforming** — return an appropriate error response (using
291
+ [`HttpProblems`](../programmable-api/http-problems.mdx)) if a parameter value
292
+ is unexpected, rather than forwarding bad data to your backend
293
+ - **Keep the policy focused** — if your transformation logic is complex,
294
+ consider splitting it into a separate utility module imported by the policy
295
+
296
+ ## Next Steps
297
+
298
+ - [URL Rewrite Handler](../handlers/url-rewrite.mdx) — full reference for
299
+ rewrite patterns and available interpolation variables
300
+ - [Custom Code Patterns](../articles/custom-code-patterns.md) — common patterns
301
+ for writing inbound policies, outbound policies, and handlers
302
+ - [ZuploContext](../programmable-api/zuplo-context.mdx) — reference for
303
+ `context.custom` and other context properties
304
+ - [ZuploRequest](../programmable-api/zuplo-request.mdx) — reference for
305
+ `request.params` and constructing new requests
306
+ - [User-Based Backend Routing](./user-based-backend-routing.mdx) — a related
307
+ pattern using `context.custom` with URL Rewrite for routing by user identity
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zuplo",
3
- "version": "6.71.10",
3
+ "version": "6.71.11",
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.71.10",
23
- "@zuplo/core": "6.71.10",
24
- "@zuplo/runtime": "6.71.10",
22
+ "@zuplo/cli": "6.71.11",
23
+ "@zuplo/core": "6.71.11",
24
+ "@zuplo/runtime": "6.71.11",
25
25
  "@zuplo/test": "1.4.0"
26
26
  }
27
27
  }