zuplo 6.74.14 → 7.0.0

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.
@@ -0,0 +1,275 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft-07/schema",
3
+ "$id": "https://cdn.zuplo.com/policies/runtime/schemas/cdn-cache-control-outbound.json",
4
+ "type": "object",
5
+ "title": "CDN Cache Control",
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": "Sets the response headers that tell a CDN how long it may cache a response and which purge tags it belongs to, separately from what the browser is told.",
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": "CdnCacheControlOutboundPolicy",
25
+ "description": "The name of the exported type"
26
+ },
27
+ "module": {
28
+ "const": "$import(@zuplo/runtime)",
29
+ "description": "The module containing the policy"
30
+ },
31
+ "options": {
32
+ "title": "CdnCacheControlOutboundPolicyOptions",
33
+ "type": "object",
34
+ "description": "The options for this policy.",
35
+ "additionalProperties": false,
36
+ "required": ["cdn"],
37
+ "properties": {
38
+ "cdn": {
39
+ "type": "string",
40
+ "title": "CdnCacheControlVendor",
41
+ "enum": [
42
+ "akamai",
43
+ "fastly",
44
+ "cloudflare",
45
+ "cloudfront",
46
+ "generic"
47
+ ],
48
+ "examples": ["akamai"],
49
+ "description": "The CDN in front of this gateway. Required, with no default — each CDN reads a different header for shared-cache TTL and purge tags, and a wrong guess fails silently. Use `generic` for an RFC 9213 CDN that is not listed."
50
+ },
51
+ "strategy": {
52
+ "type": "string",
53
+ "title": "CdnCacheControlStrategy",
54
+ "default": "targeted",
55
+ "enum": ["targeted", "s-maxage"],
56
+ "x-advanced": true,
57
+ "description": "How the edge TTL is carried. `targeted` uses the CDN's own header (`Edge-Control`, `Surrogate-Control`, `CDN-Cache-Control`) and is the only way to cache at the edge without also caching in the browser. `s-maxage` folds the edge TTL into `Cache-Control` instead, for a property that honors origin `Cache-Control` but not the targeted header. Defaults to `targeted`, except on a CDN that reads no targeted header — CloudFront defaults to `s-maxage`, which is the only thing it supports."
58
+ },
59
+ "edge": {
60
+ "type": "object",
61
+ "title": "CdnCacheControlEdgeOptions",
62
+ "additionalProperties": false,
63
+ "required": ["maxAge"],
64
+ "description": "How long the CDN may serve this response. These directives are sent only to the CDN, never to the browser (unless `strategy` is `s-maxage`).",
65
+ "properties": {
66
+ "maxAge": {
67
+ "type": "integer",
68
+ "minimum": 0,
69
+ "examples": [1800],
70
+ "description": "Seconds the CDN may serve this response without revalidating."
71
+ },
72
+ "staleWhileRevalidate": {
73
+ "type": "integer",
74
+ "minimum": 0,
75
+ "examples": [600],
76
+ "description": "Seconds the CDN may keep serving a stale response while it revalidates in the background. Not expressible in Akamai's `Edge-Control` — configure it in Property Manager, or use `strategy: s-maxage`."
77
+ },
78
+ "staleIfError": {
79
+ "type": "integer",
80
+ "minimum": 0,
81
+ "examples": [86400],
82
+ "description": "Seconds the CDN may serve a stale response when the gateway returns an error. Not expressible in Akamai's `Edge-Control` — configure it in Property Manager, or use `strategy: s-maxage`."
83
+ }
84
+ }
85
+ },
86
+ "client": {
87
+ "type": "object",
88
+ "title": "CdnCacheControlClientOptions",
89
+ "additionalProperties": false,
90
+ "required": [],
91
+ "description": "The `Cache-Control` sent to the browser or app. Separate from `edge` so the CDN can absorb load without clients holding stale data.",
92
+ "properties": {
93
+ "maxAge": {
94
+ "type": "integer",
95
+ "minimum": 0,
96
+ "examples": [60],
97
+ "description": "Seconds the client may reuse this response. Omit to send no `max-age`."
98
+ },
99
+ "visibility": {
100
+ "type": "string",
101
+ "title": "CdnCacheControlClientVisibility",
102
+ "enum": ["public", "private", "no-store"],
103
+ "examples": ["public"],
104
+ "description": "`public` allows any cache to store the response, `private` restricts it to the client's own cache, `no-store` forbids client caching entirely. With `strategy: targeted` a `private`/`no-store` client policy still permits edge caching, because the CDN reads its own header."
105
+ },
106
+ "noCache": {
107
+ "type": "boolean",
108
+ "default": false,
109
+ "x-advanced": true,
110
+ "description": "Adds `no-cache`, so the client must revalidate before reusing the response. Distinct from `visibility: no-store`, which forbids storing it at all."
111
+ },
112
+ "mustRevalidate": {
113
+ "type": "boolean",
114
+ "default": false,
115
+ "x-advanced": true,
116
+ "description": "Adds `must-revalidate`, so the client may not serve the response once stale."
117
+ },
118
+ "mode": {
119
+ "type": "string",
120
+ "title": "CdnCacheControlClientMode",
121
+ "default": "strictest",
122
+ "enum": ["strictest", "replace", "preserve"],
123
+ "description": "How to combine the configured client policy with a `Cache-Control` the upstream already sent. `strictest` (default) emits the more conservative of the two — the lower `max-age` and the union of restrictive directives. `replace` overwrites whatever upstream sent. `preserve` leaves the upstream header untouched and only adds the CDN headers."
124
+ }
125
+ }
126
+ },
127
+ "tags": {
128
+ "type": "array",
129
+ "items": {
130
+ "type": "string"
131
+ },
132
+ "default": [],
133
+ "examples": [["catalog", "cities"]],
134
+ "description": "Purge tags, rendered in the target CDN's format: comma-joined `Edge-Cache-Tag` (Akamai), space-joined `Surrogate-Key` (Fastly), comma-joined `Cache-Tag` (Cloudflare). CloudFront has no tag purge and rejects this option. Tags are validated against the CDN's length and character limits, because an over-long tag header is silently truncated rather than rejected."
135
+ },
136
+ "vary": {
137
+ "type": "array",
138
+ "items": {
139
+ "type": "string"
140
+ },
141
+ "default": [],
142
+ "examples": [["accept-language"]],
143
+ "x-advanced": true,
144
+ "description": "Request headers that vary this response, unioned with any `Vary` the upstream already sent and emitted as `Vary`. The union is deliberate: replacing an upstream `Vary: Authorization` would stop a shared cache keying on it and let it serve one user's response to another. Off by default: Akamai skips caching any response carrying `Vary` until Cache ID Modification is configured, so enabling this can silently disable edge caching entirely."
145
+ },
146
+ "respectUpstream": {
147
+ "type": "boolean",
148
+ "default": true,
149
+ "x-advanced": true,
150
+ "description": "When true (default), an upstream `Cache-Control` of `no-store`/`no-cache`/`private`, or a `Set-Cookie` on the response, suppresses all edge headers and purge tags. This keeps the application's ability to mark a response uncacheable. Set to false to override it — on Akamai this emits `Edge-Control: !no-store`."
151
+ },
152
+ "cacheConfig": {
153
+ "type": "object",
154
+ "title": "CdnCacheControlConfigFunction",
155
+ "additionalProperties": false,
156
+ "x-advanced": true,
157
+ "required": ["export", "module"],
158
+ "description": "A function that overrides the cache configuration per response, for rules that cannot be expressed statically (per-entity purge tags, a TTL that depends on a response header, or skipping an empty result set). Returning nothing (`undefined` or `null`) falls back to the `edge`, `client` and `tags` options, so those stay the enforced default and the function only handles exceptions. A returned config \\*\\*replaces\\*\\* those options wholesale rather than being merged directive by directive, so include every directive the response needs. Return `{ cache: false }` to skip caching a response entirely — note that also sends `Cache-Control: no-store` to the client unless `client.mode` is `preserve`. The function receives the live response: it must not consume the body, and should call `response.clone()` if it needs to read it.",
159
+ "properties": {
160
+ "export": {
161
+ "type": "string",
162
+ "default": "default",
163
+ "description": "Specifies the export that holds your cache config function, e.g. `default`, `cdnCacheConfig`.",
164
+ "examples": ["default", "cdnCacheConfig"]
165
+ },
166
+ "module": {
167
+ "type": "string",
168
+ "default": "",
169
+ "description": "Specifies the module that holds your cache config function, in the format `$import(./modules/my-module)`.",
170
+ "examples": ["$import(./modules/my-module)"]
171
+ }
172
+ }
173
+ }
174
+ },
175
+ "if": {
176
+ "required": ["cdn"],
177
+ "properties": {
178
+ "cdn": {
179
+ "const": "cloudfront"
180
+ }
181
+ }
182
+ },
183
+ "then": {
184
+ "properties": {
185
+ "strategy": {
186
+ "const": "s-maxage"
187
+ },
188
+ "tags": {
189
+ "maxItems": 0
190
+ }
191
+ }
192
+ },
193
+ "else": {
194
+ "allOf": [
195
+ {
196
+ "if": {
197
+ "required": ["cdn"],
198
+ "properties": {
199
+ "cdn": {
200
+ "const": "generic"
201
+ }
202
+ }
203
+ },
204
+ "then": {
205
+ "properties": {
206
+ "tags": {
207
+ "maxItems": 0
208
+ }
209
+ }
210
+ }
211
+ },
212
+ {
213
+ "if": {
214
+ "required": ["cdn"],
215
+ "properties": {
216
+ "cdn": {
217
+ "const": "akamai"
218
+ }
219
+ },
220
+ "not": {
221
+ "required": ["strategy"],
222
+ "properties": {
223
+ "strategy": {
224
+ "const": "s-maxage"
225
+ }
226
+ }
227
+ }
228
+ },
229
+ "then": {
230
+ "properties": {
231
+ "edge": {
232
+ "properties": {
233
+ "staleWhileRevalidate": false,
234
+ "staleIfError": false
235
+ }
236
+ }
237
+ }
238
+ }
239
+ }
240
+ ]
241
+ }
242
+ }
243
+ },
244
+ "examples": [
245
+ {
246
+ "export": "CdnCacheControlOutboundPolicy",
247
+ "module": "$import(@zuplo/runtime)",
248
+ "options": {
249
+ "cacheConfig": {
250
+ "export": "default",
251
+ "module": "$import(./modules/my-module)"
252
+ },
253
+ "cdn": "akamai",
254
+ "client": {
255
+ "maxAge": 60,
256
+ "mode": "strictest",
257
+ "mustRevalidate": false,
258
+ "noCache": false,
259
+ "visibility": "public"
260
+ },
261
+ "edge": {
262
+ "maxAge": 1800,
263
+ "staleIfError": 86400,
264
+ "staleWhileRevalidate": 600
265
+ },
266
+ "respectUpstream": true,
267
+ "strategy": "targeted",
268
+ "tags": ["catalog", "cities"],
269
+ "vary": ["accept-language"]
270
+ }
271
+ }
272
+ ]
273
+ }
274
+ }
275
+ }
@@ -0,0 +1,234 @@
1
+ This policy compares the client's IP address against an allow list, a deny list,
2
+ or both, and rejects non-matching requests with a `403 Forbidden`. Entries are
3
+ individual IP addresses or CIDR ranges, in IPv4 or IPv6.
4
+
5
+ ### How It Works
6
+
7
+ For each request the policy:
8
+
9
+ 1. Determines the client IP address — the same one reported to your code as
10
+ `context.incomingRequestProperties.ip` — or reads it from `clientIpHeader`
11
+ when that option is set
12
+ 2. Rejects the request if the address matches any `deny` entry
13
+ 3. Rejects the request if an `allow` list is set and the address matches none of
14
+ its entries
15
+ 4. Otherwise passes the request to the next policy or the handler
16
+
17
+ `deny` is evaluated first, so an address covered by both lists is rejected.
18
+
19
+ At least one of `allow` or `deny` must be set — a policy with neither would let
20
+ every request through, which is never what was intended.
21
+
22
+ ### Policy Configuration
23
+
24
+ Allow only two networks and a single address:
25
+
26
+ ```json
27
+ {
28
+ "name": "ip-address-restriction",
29
+ "policyType": "ip-address-restriction-inbound",
30
+ "handler": {
31
+ "export": "IpAddressRestrictionInboundPolicy",
32
+ "module": "$import(@zuplo/runtime)",
33
+ "options": {
34
+ "allow": ["198.51.100.0/24", "2001:db8:1234::/48", "203.0.113.42"]
35
+ }
36
+ }
37
+ }
38
+ ```
39
+
40
+ Block a single abusive network while leaving the route otherwise public:
41
+
42
+ ```json
43
+ {
44
+ "name": "ip-address-restriction",
45
+ "policyType": "ip-address-restriction-inbound",
46
+ "handler": {
47
+ "export": "IpAddressRestrictionInboundPolicy",
48
+ "module": "$import(@zuplo/runtime)",
49
+ "options": {
50
+ "deny": ["192.0.2.0/24"]
51
+ }
52
+ }
53
+ }
54
+ ```
55
+
56
+ ### Writing Entries
57
+
58
+ - An entry without a prefix is a single address: `203.0.113.42` is the same as
59
+ `203.0.113.42/32`, and `2001:db8::1` the same as `2001:db8::1/128`.
60
+ - Host bits beyond the prefix are ignored, so `10.1.2.3/8` and `10.0.0.0/8` mean
61
+ the same range.
62
+ - IPv4-mapped IPv6 addresses are matched as the IPv4 address they map to. A
63
+ client that a dual-stack proxy reports as `::ffff:198.51.100.7` matches an
64
+ `198.51.100.0/24` entry.
65
+ - An IPv4 address never matches an IPv6 range, and vice versa. To cover a client
66
+ reachable over both, list a range for each family.
67
+ - Addresses with leading zeros in an octet (`010.0.0.1`) are rejected, because
68
+ different tools read them as different addresses.
69
+
70
+ Entries are validated when the policy is created, so a malformed address or
71
+ prefix fails your deployment rather than rejecting live traffic. Each list can
72
+ also be sourced from environment variables per entry:
73
+
74
+ ```json
75
+ {
76
+ "options": {
77
+ "allow": ["$env(OFFICE_CIDR)", "$env(VPN_CIDR)"]
78
+ }
79
+ }
80
+ ```
81
+
82
+ ### Usage Example
83
+
84
+ Restrict an admin route while leaving the rest of the API public:
85
+
86
+ ```json
87
+ {
88
+ "paths": {
89
+ "/v1/admin/users": {
90
+ "get": {
91
+ "x-zuplo-route": {
92
+ "policies": {
93
+ "inbound": ["ip-address-restriction", "api-key-auth"]
94
+ },
95
+ "handler": {
96
+ "export": "urlForwardHandler",
97
+ "module": "$import(@zuplo/runtime)",
98
+ "options": {
99
+ "baseUrl": "https://api.internal.example.com"
100
+ }
101
+ }
102
+ }
103
+ }
104
+ }
105
+ }
106
+ }
107
+ ```
108
+
109
+ Put this policy first in the inbound list so restricted addresses are rejected
110
+ before any work — authentication, rate limiting, upstream token fetches — is
111
+ done on their behalf.
112
+
113
+ ### Where the Default Address Comes From
114
+
115
+ By default the policy uses the address the gateway itself resolved — the same
116
+ value your code sees as `context.incomingRequestProperties.ip`. That comes from
117
+ exactly **one** header, chosen by the platform running the gateway
118
+ (`true-client-ip` on Cloudflare, `x-real-ip` elsewhere), and it is resolved from
119
+ the request as it originally arrived.
120
+
121
+ Two consequences worth knowing:
122
+
123
+ - **There is no fallback to a second header.** If the trusted header is absent,
124
+ the address is reported as unknown rather than being guessed from
125
+ `x-forwarded-for` or another header. Every available fallback is one a caller
126
+ can write, so a fallback would let anyone satisfy an allow list by sending a
127
+ header.
128
+ - **Header stripping does not affect it.** The gateway removes `x-real-ip` and
129
+ the `cf-*` headers from the request before policies run (from compatibility
130
+ date **2024-01-15**), but the default path reads the original request, so this
131
+ makes no difference to it. It _does_ affect `clientIpHeader` — see below.
132
+
133
+ During local development there is no edge to set the header and the caller
134
+ really is the loopback interface, so the address is reported as `127.0.0.1`. An
135
+ allow list that does not cover `127.0.0.1` will reject your local requests — add
136
+ it to the list to test locally:
137
+
138
+ ```json
139
+ {
140
+ "options": {
141
+ "allow": ["198.51.100.0/24", "127.0.0.1", "::1"]
142
+ }
143
+ }
144
+ ```
145
+
146
+ Note that `allowUnknownIpAddress` does **not** help here. Locally the address is
147
+ known — it is `127.0.0.1` — and that option only applies when no address could
148
+ be determined at all.
149
+
150
+ ### Requests With No Client IP Address
151
+
152
+ When no client IP address can be determined, the request is rejected if an
153
+ `allow` list is set, since an unknown address cannot be on it. With only a
154
+ `deny` list the request continues, because there is nothing for it to match.
155
+
156
+ Set `allowUnknownIpAddress` to `true` to let these requests through even with an
157
+ `allow` list. This is most useful when you run your own proxy and cannot
158
+ guarantee it sets the trusted header on every request.
159
+
160
+ ### Custom Client IP Header
161
+
162
+ If a proxy in front of the gateway reports the client address in a header other
163
+ than the standard ones, name it with `clientIpHeader`:
164
+
165
+ ```json
166
+ {
167
+ "options": {
168
+ "allow": ["198.51.100.0/24"],
169
+ "clientIpHeader": "x-client-ip"
170
+ }
171
+ }
172
+ ```
173
+
174
+ Only the named header is read when this is set. Prefer a header your proxy sets
175
+ to a single address.
176
+
177
+ Unlike the default path, this option reads the request as policies see it, after
178
+ the gateway has stripped `x-real-ip` and the `cf-*` headers (from compatibility
179
+ date **2024-01-15**). Naming one of those here resolves nothing, and with an
180
+ `allow` list that rejects every request. Use a dedicated header your proxy sets,
181
+ such as `x-client-ip`.
182
+
183
+ If the header holds a comma-separated list, the **last** entry is used, because
184
+ proxies append and so the nearest proxy's entry is the rightmost one —
185
+ everything to its left is whatever the caller sent. For a single-value header
186
+ this makes no difference. This assumes one appending proxy in front of the
187
+ gateway; if you have more than one, use a dedicated single-value header, since
188
+ the trustworthy entry is then further left and its position depends on how many
189
+ hops you control.
190
+
191
+ ### Security Considerations
192
+
193
+ :::warning
194
+
195
+ This policy is only as trustworthy as the header it reads the client address
196
+ from. A header is a security control **only** if the proxy in front of your
197
+ gateway replaces any client-supplied value rather than appending to it.
198
+
199
+ The default path is not configurable for this reason: it reads a single header
200
+ designated by the platform running the gateway, and never falls back to another
201
+ one. On Zuplo's managed edge that header is set by the edge on every request and
202
+ a caller cannot override it.
203
+
204
+ Two specifics worth knowing if you self-host or run a CDN in front of Zuplo:
205
+
206
+ - **`true-client-ip` is spoofable unless your own edge sets it.** Cloudflare
207
+ documents this directly: in a stacked-CDN setup, if you do not add the header
208
+ yourself, "its value can be spoofed to any value".
209
+ - **`x-forwarded-for` is a list, and the untrustworthy end comes first.** When a
210
+ request already carries an `X-Forwarded-For` header,
211
+ [Cloudflare appends to it](https://developers.cloudflare.com/fundamentals/reference/http-headers/#x-forwarded-for)
212
+ rather than replacing it, and most proxies behave the same way. The entry your
213
+ own proxy added is therefore at the **end** of the list, while the entries at
214
+ the front are whatever the caller chose to send. The policy never reads this
215
+ header on the default path — but if you point `clientIpHeader` at it, this is
216
+ why only the last entry is used.
217
+
218
+ If a proxy you control reports the client address in a dedicated header that it
219
+ always overwrites, name that header with `clientIpHeader`. That is the
220
+ configuration to prefer whenever you have it, because it removes the ambiguity
221
+ entirely.
222
+
223
+ :::
224
+
225
+ - Rejected requests get a `403 Forbidden` with no detail about the address seen
226
+ or the rule matched. The address and the matching entry are written to the
227
+ request log at debug level instead, so an unauthorized caller is not told what
228
+ the gateway sees or that it is filtering by address at all.
229
+ - An IP address identifies a network path, not a caller. Combine this policy
230
+ with an authentication policy rather than using it as your only control —
231
+ addresses within an allowed range can be shared, reassigned, or spoofed
232
+ upstream of your gateway.
233
+ - Prefer the narrowest ranges that work. A `/8` allow entry covers 16 million
234
+ addresses.
@@ -0,0 +1,17 @@
1
+ Restrict which clients can reach a route based on their IP address. Configure an
2
+ allow list to expose an endpoint only to a set of known networks, a deny list to
3
+ shut out abusive traffic, or both. Entries can be individual IP addresses or
4
+ CIDR ranges, in IPv4 or IPv6.
5
+
6
+ With this policy, you'll benefit from:
7
+
8
+ - **Private APIs on a Public Gateway**: Limit admin, partner, or internal
9
+ endpoints to your office, VPN, or data center ranges
10
+ - **Immediate Blocking**: Drop traffic from a bad actor's address or subnet
11
+ without a deploy of your backend
12
+ - **Per-Route Control**: Apply different lists to different routes, so a public
13
+ endpoint and a restricted one can live in the same API
14
+ - **IPv4 and IPv6**: Single addresses and CIDR ranges in both families, with
15
+ IPv4-mapped IPv6 addresses handled automatically
16
+ - **Fail-Closed Defaults**: Requests whose address cannot be determined are
17
+ rejected when an allow list is set
@@ -0,0 +1,97 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft-07/schema",
3
+ "$id": "https://cdn.zuplo.com/policies/runtime/schemas/ip-address-restriction-inbound.json",
4
+ "type": "object",
5
+ "title": "IP Address Restriction",
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": "Restricts which client IP addresses can call a route using an allow list, a deny list, or both. Entries are individual IP addresses or CIDR ranges, in IPv4 or IPv6.",
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": "IpAddressRestrictionInboundPolicy",
25
+ "description": "The name of the exported type"
26
+ },
27
+ "module": {
28
+ "const": "$import(@zuplo/runtime)",
29
+ "description": "The module containing the policy"
30
+ },
31
+ "options": {
32
+ "title": "IpAddressRestrictionInboundPolicyOptions",
33
+ "type": "object",
34
+ "description": "The options for this policy.",
35
+ "additionalProperties": false,
36
+ "required": [],
37
+ "if": {
38
+ "not": {
39
+ "required": ["allow"]
40
+ }
41
+ },
42
+ "then": {
43
+ "required": ["deny"]
44
+ },
45
+ "properties": {
46
+ "allow": {
47
+ "type": "array",
48
+ "minItems": 1,
49
+ "items": {
50
+ "type": "string",
51
+ "pattern": "^\\s*([^,\\s]*\\$env\\([^)]+\\)[^,\\s]*|[0-9A-Fa-f.:]+(/[0-9]{1,3})?)\\s*$",
52
+ "description": "An IP address (`203.0.113.42`, `2001:db8::1`) or a CIDR range (`198.51.100.0/24`, `2001:db8::/32`)."
53
+ },
54
+ "examples": [["203.0.113.42", "198.51.100.0/24"]],
55
+ "description": "The IP addresses and CIDR ranges that are allowed to call the route. When set, requests from every other address are rejected with a `403 Forbidden`."
56
+ },
57
+ "deny": {
58
+ "type": "array",
59
+ "minItems": 1,
60
+ "items": {
61
+ "type": "string",
62
+ "pattern": "^\\s*([^,\\s]*\\$env\\([^)]+\\)[^,\\s]*|[0-9A-Fa-f.:]+(/[0-9]{1,3})?)\\s*$",
63
+ "description": "An IP address (`203.0.113.42`, `2001:db8::1`) or a CIDR range (`198.51.100.0/24`, `2001:db8::/32`)."
64
+ },
65
+ "examples": [["203.0.113.99", "192.0.2.0/24"]],
66
+ "description": "The IP addresses and CIDR ranges that are rejected with a `403 Forbidden`. Evaluated before `allow`, so an address matching both lists is rejected."
67
+ },
68
+ "clientIpHeader": {
69
+ "type": "string",
70
+ "examples": ["x-client-ip"],
71
+ "x-show-example": false,
72
+ "x-advanced": true,
73
+ "description": "Read the client IP address from this header instead of the standard forwarding headers (`x-real-ip`, `true-client-ip`, `cf-connecting-ip`, then the first entry of `x-forwarded-for`). Only set this when a proxy in front of the gateway reports the client address in a non-standard header."
74
+ },
75
+ "allowUnknownIpAddress": {
76
+ "type": "boolean",
77
+ "default": false,
78
+ "x-show-example": false,
79
+ "x-advanced": true,
80
+ "description": "Allow requests whose client IP address cannot be determined. By default these requests are rejected when an `allow` list is set, because an unknown address cannot be on it."
81
+ }
82
+ }
83
+ }
84
+ },
85
+ "examples": [
86
+ {
87
+ "export": "IpAddressRestrictionInboundPolicy",
88
+ "module": "$import(@zuplo/runtime)",
89
+ "options": {
90
+ "allow": ["203.0.113.42", "198.51.100.0/24"],
91
+ "deny": ["203.0.113.99", "192.0.2.0/24"]
92
+ }
93
+ }
94
+ ]
95
+ }
96
+ }
97
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zuplo",
3
- "version": "6.74.14",
3
+ "version": "7.0.0",
4
4
  "type": "module",
5
5
  "description": "The programmable API Gateway",
6
6
  "author": "Zuplo, Inc.",
@@ -12,16 +12,16 @@
12
12
  "docs"
13
13
  ],
14
14
  "engines": {
15
- "node": ">=20.16.0",
15
+ "node": ">=24.0.0",
16
16
  "npm": ">=10"
17
17
  },
18
18
  "bin": {
19
19
  "zuplo": "zuplo.js"
20
20
  },
21
21
  "dependencies": {
22
- "@zuplo/cli": "6.74.14",
23
- "@zuplo/core": "6.74.14",
24
- "@zuplo/runtime": "6.74.14",
25
- "@zuplo/test": "6.74.14"
22
+ "@zuplo/cli": "7.0.0",
23
+ "@zuplo/core": "7.0.0",
24
+ "@zuplo/runtime": "7.0.0",
25
+ "@zuplo/test": "7.0.0"
26
26
  }
27
27
  }