zuplo 6.71.26 → 6.72.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.
- package/docs/articles/accounts/audit-logs.mdx +0 -7
- package/docs/articles/securing-backend-mtls.mdx +3 -0
- package/docs/dedicated/federated-gateways.mdx +1 -8
- package/docs/mcp-gateway/cross-app-access/overview.mdx +0 -7
- package/docs/mcp-server/openai-apps-sdk.mdx +0 -7
- package/docs/policies/_index.md +2 -2
- package/docs/policies/audit-log-inbound/doc.md +117 -58
- package/docs/policies/audit-log-inbound/schema.json +55 -35
- package/docs/policies/mcp-capability-filter-inbound/doc.md +132 -0
- package/docs/policies/mcp-capability-filter-inbound/schema.json +138 -1
- package/docs/policies/monetization-inbound/doc.md +29 -0
- package/package.json +4 -4
|
@@ -10,13 +10,6 @@ account, helping you track changes and maintain compliance requirements. Every
|
|
|
10
10
|
change performed in your account is logged with detailed information about who
|
|
11
11
|
performed the change, when it occurred, and what resources were affected.
|
|
12
12
|
|
|
13
|
-
:::note{title="Beta Feature"}
|
|
14
|
-
|
|
15
|
-
Audit logs are currently in beta. During the beta period not all events may be
|
|
16
|
-
captured. Full coverage is in progress.
|
|
17
|
-
|
|
18
|
-
:::
|
|
19
|
-
|
|
20
13
|
## Overview
|
|
21
14
|
|
|
22
15
|
Audit logs capture critical events across your Zuplo infrastructure, including:
|
|
@@ -263,6 +263,9 @@ For more information on securing your backend, see:
|
|
|
263
263
|
Alternative approach using shared secrets
|
|
264
264
|
- [Secure Tunnels](./secure-tunnel.mdx) - Connect to private backends without
|
|
265
265
|
exposing them to the internet
|
|
266
|
+
- [Client mTLS Authentication](./securing-the-gateway-with-client-mtls.mdx) -
|
|
267
|
+
The reverse direction, where clients authenticate to your Zuplo gateway with a
|
|
268
|
+
client certificate
|
|
266
269
|
|
|
267
270
|
If you need assistance configuring mTLS for your project, contact us at
|
|
268
271
|
[support@zuplo.com](mailto:support@zuplo.com).
|
|
@@ -3,13 +3,6 @@ title: "Managed Dedicated: Federated Gateways"
|
|
|
3
3
|
sidebar_label: Federated Gateways
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
:::note{title="Beta Feature"}
|
|
7
|
-
|
|
8
|
-
This feature is in Beta - please use with care and provide feedback to the team
|
|
9
|
-
if you encounter any issues.
|
|
10
|
-
|
|
11
|
-
:::
|
|
12
|
-
|
|
13
6
|
<EnterpriseFeature name="Federated Gateways" />
|
|
14
7
|
|
|
15
8
|
With a managed dedicated Zuplo instance you can create a federated gateway that
|
|
@@ -30,7 +23,7 @@ Federated gateways are useful for several reasons:
|
|
|
30
23
|
- **Versioning**: You can use federated gateways to manage different versions of
|
|
31
24
|
your API. For example, if you wanted to run a new version of your API
|
|
32
25
|
alongside the old one and route requests to specific versions based on user or
|
|
33
|
-
other context you can keep different versions in separate Zuplo projects.
|
|
26
|
+
other context, you can keep different versions in separate Zuplo projects.
|
|
34
27
|
- **Risk Minimization**: By separating different parts of your API into
|
|
35
28
|
different Zuplo projects, you can reduce the risk of causing inadvertent
|
|
36
29
|
changes to other parts of the API. This can be especially useful if your API
|
|
@@ -8,13 +8,6 @@ description: |
|
|
|
8
8
|
have to.
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
:::note{title="Beta"}
|
|
12
|
-
|
|
13
|
-
Cross App Access support is in beta. The configuration model and policy options
|
|
14
|
-
may change before general availability.
|
|
15
|
-
|
|
16
|
-
:::
|
|
17
|
-
|
|
18
11
|
Cross App Access (XAA) lets one application reach another application's API on a
|
|
19
12
|
user's behalf **through the identity provider both apps already trust** —
|
|
20
13
|
instead of running a separate, point-to-point OAuth connection between them. For
|
|
@@ -21,13 +21,6 @@ emits the UI.
|
|
|
21
21
|
|
|
22
22
|
:::
|
|
23
23
|
|
|
24
|
-
:::warning
|
|
25
|
-
|
|
26
|
-
The OpenAI Apps SDK and support for it in Zuplo's `mcpServerHandler` is in beta
|
|
27
|
-
and subject to change!
|
|
28
|
-
|
|
29
|
-
:::
|
|
30
|
-
|
|
31
24
|
## `tools`
|
|
32
25
|
|
|
33
26
|
MCP tools define the functionality of your app. Zuplo MCP servers support tools
|
package/docs/policies/_index.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
| archive-request-gcp-storage-inbound | Archive Request to GCP Storage | Archive the incoming request to Google Cloud Storage. | api-gateway |
|
|
18
18
|
| archive-response-aws-s3-outbound | Archive Response to AWS S3 | Archive the outgoing response body to AWS S3 storage | api-gateway |
|
|
19
19
|
| archive-response-azure-storage-outbound | Archive Response to Azure Storage | Archive the outgoing response to Azure blob storage. | api-gateway |
|
|
20
|
-
| audit-log-inbound | Audit Logs | Capture detailed logs of requests for auditing purposes. | api-gateway |
|
|
20
|
+
| audit-log-inbound | Audit Logs | Capture detailed logs of requests for auditing purposes. The policy emits one structured CloudEvent per request, and user code can emit additional events via the static `log()` method. | api-gateway |
|
|
21
21
|
| auth0-jwt-auth-inbound | Auth0 JWT Auth | Authenticate users using Auth0 issued JWT tokens. | api-gateway |
|
|
22
22
|
| authzen-inbound | AuthZEN Authorization | Authorize requests using an AuthZEN compatible PDP | api-gateway |
|
|
23
23
|
| cognito-jwt-auth-inbound | AWS Cognito JWT Auth | Authenticate requests with JWT tokens issued by AWS Cognito. | api-gateway |
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
| ldap-auth-inbound | LDAP Auth | Authenticate requests using an LDAP server. | api-gateway |
|
|
58
58
|
| mcp-cognito-oauth-inbound | MCP Amazon Cognito OAuth | Authenticate MCP gateway requests using a gateway-issued OAuth access token, with browser login delegated to Amazon Cognito. Cognito-friendly wrapper around `McpOAuthInboundPolicy`. Provide an AWS region, user pool id, user pool domain, client id, and client secret; the constructor derives the Cognito issuer, JWKS URL, authorize URL, and token URL. | mcp-gateway |
|
|
59
59
|
| mcp-auth0-oauth-inbound | MCP Auth0 OAuth | Authenticate MCP gateway requests using a gateway-issued OAuth access token, with browser login delegated to Auth0. Auth0-friendly wrapper around `McpOAuthInboundPolicy`. Provide `auth0Domain` and `clientId`; the constructor derives the OIDC issuer, JWKS URL, and Auth0 authorize/token endpoints automatically and runs the resulting shape through the same Zod schema as the generic policy. Validation runs lazily inside the policy constructor, which the runtime caches per policy name — so a misconfigured policy fails the first request with a `ConfigurationError` (surfaced in the 500 problem body) rather than crashing boot. | mcp-gateway |
|
|
60
|
-
| mcp-capability-filter-inbound | MCP Capability Filter | Curate MCP capabilities advertised and reachable through `McpProxyHandler`. Validation runs lazily inside the policy constructor, which the runtime caches per policy name. Misconfigured options therefore fail on first use with a customer-facing `ConfigurationError` instead of failing at module load. | mcp-gateway |
|
|
60
|
+
| mcp-capability-filter-inbound | MCP Capability Filter | Curate MCP capabilities advertised and reachable through `McpProxyHandler`. Validation runs lazily inside the policy constructor, which the runtime caches per policy name. Misconfigured options therefore fail on first use with a customer-facing `ConfigurationError` instead of failing at module load. Optional per-caller access control (`accessControl`) narrows the curated catalog. See `claimsCapabilityResolver` for built-in role/group matching and `McpCapabilityResolver` for custom resolvers. | mcp-gateway |
|
|
61
61
|
| mcp-clerk-oauth-inbound | MCP Clerk OAuth | Authenticate MCP gateway requests using a gateway-issued OAuth access token, with browser login delegated to Clerk. Clerk-friendly wrapper around `McpOAuthInboundPolicy`. Provide Clerk's Frontend API URL plus the OAuth application client id and secret; the constructor derives the Clerk issuer, JWKS URL, authorize URL, and token URL. | mcp-gateway |
|
|
62
62
|
| mcp-google-oauth-inbound | MCP Google OAuth | Authenticate MCP gateway requests using a gateway-issued OAuth access token, with browser login delegated to Google. Google-friendly wrapper around `McpOAuthInboundPolicy`. Provide `clientId` and `clientSecret`; the constructor uses Google's fixed OIDC issuer, JWKS URL, authorize URL, and token URL, then runs the resulting shape through the same Zod schema as the generic policy. | mcp-gateway |
|
|
63
63
|
| mcp-keycloak-oauth-inbound | MCP Keycloak OAuth | Authenticate MCP gateway requests using a gateway-issued OAuth access token, with browser login delegated to Keycloak. Keycloak-friendly wrapper around `McpOAuthInboundPolicy`. Provide the Keycloak server root, realm, client id, and client secret; the constructor derives the realm issuer, JWKS URL, authorize URL, and token URL from Keycloak's documented OIDC endpoint layout. | mcp-gateway |
|
|
@@ -1,78 +1,137 @@
|
|
|
1
|
-
##
|
|
1
|
+
## Automatic Request Auditing
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Once the Audit Logs policy is added to a route, it automatically records one
|
|
4
|
+
structured audit event for every request that passes through it — no extra code
|
|
5
|
+
required. Each event captures who made the request, what they called, and the
|
|
6
|
+
outcome.
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
The automatic event uses the type `com.zuplo.api.request` and includes the
|
|
9
|
+
actor, the HTTP method and path, the response status, and the geolocation of the
|
|
10
|
+
caller.
|
|
6
11
|
|
|
7
|
-
|
|
8
|
-
|
|
12
|
+
For very high-volume routes you can record only a fraction of requests by
|
|
13
|
+
setting the `samplingRate` option (for example `0.1` to capture 10% of
|
|
14
|
+
requests).
|
|
9
15
|
|
|
10
|
-
|
|
16
|
+
## Controlling What's Logged
|
|
11
17
|
|
|
12
|
-
|
|
13
|
-
|
|
18
|
+
Audit events can contain personal or sensitive data. To keep audit logging
|
|
19
|
+
compliant with your own data-handling and PII policies, use the `include`
|
|
20
|
+
option to turn individual fields off. Everything is captured by default; set a
|
|
21
|
+
flag to `false` to omit it:
|
|
14
22
|
|
|
15
|
-
|
|
23
|
+
- `queryParams` — the request's query-string parameters (in the logged URL).
|
|
24
|
+
- `user` — the authenticated user / actor identity (subject, email,
|
|
25
|
+
connection). The `user` vs `anonymous` classification is always kept, but the
|
|
26
|
+
identifying values are omitted when disabled.
|
|
27
|
+
- `ipAddress` — the caller's IP address.
|
|
28
|
+
- `geolocation` — the caller's country, region, and city.
|
|
16
29
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"name": "audit-logs-inbound",
|
|
33
|
+
"policyType": "audit-logs",
|
|
34
|
+
"handler": {
|
|
35
|
+
"export": "AuditLogInboundPolicy",
|
|
36
|
+
"module": "$import(@zuplo/runtime)",
|
|
37
|
+
"options": {
|
|
38
|
+
"include": {
|
|
39
|
+
"queryParams": false,
|
|
40
|
+
"ipAddress": false
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
23
44
|
}
|
|
24
45
|
```
|
|
25
46
|
|
|
26
|
-
|
|
47
|
+
These settings also apply to the geolocation automatically added to custom
|
|
48
|
+
events emitted via `log()`.
|
|
27
49
|
|
|
28
|
-
|
|
50
|
+
## Emitting Custom Events
|
|
29
51
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
52
|
+
Beyond the automatic per-request event, you can record your own domain events —
|
|
53
|
+
for example "account deleted" or "API key rotated" — from a handler or custom
|
|
54
|
+
policy using the static `AuditLogInboundPolicy.log()` method. Any fields you
|
|
55
|
+
don't supply are filled in automatically from the request context (event id,
|
|
56
|
+
time, actor, request id, IP address, user agent, and so on).
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
// module - ./modules/audit.ts
|
|
60
|
+
|
|
61
|
+
import {
|
|
62
|
+
AuditLogInboundPolicy,
|
|
63
|
+
ZuploContext,
|
|
64
|
+
ZuploRequest,
|
|
65
|
+
} from "@zuplo/runtime";
|
|
66
|
+
|
|
67
|
+
export async function deleteAccount(
|
|
68
|
+
request: ZuploRequest,
|
|
69
|
+
context: ZuploContext
|
|
70
|
+
) {
|
|
71
|
+
const accountId = request.params.accountId;
|
|
72
|
+
|
|
73
|
+
// ...perform the delete...
|
|
74
|
+
|
|
75
|
+
AuditLogInboundPolicy.log(context, {
|
|
76
|
+
type: "com.acme.account.deleted",
|
|
77
|
+
subject: `account|${accountId}`,
|
|
78
|
+
resources: [{ type: "account", id: accountId }],
|
|
79
|
+
success: true,
|
|
80
|
+
data: { reason: "user-initiated" },
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return new Response(null, { status: 204 });
|
|
61
84
|
}
|
|
62
85
|
```
|
|
63
86
|
|
|
64
|
-
|
|
87
|
+
The event object passed to `log()` supports the following fields. Only `type` is
|
|
88
|
+
required.
|
|
65
89
|
|
|
66
|
-
|
|
67
|
-
|
|
90
|
+
- `type` — the event type, in reverse-DNS form (e.g. `com.acme.account.deleted`).
|
|
91
|
+
- `subject` — the primary entity the event is about.
|
|
92
|
+
- `resources` — an array of `{ type, id, metadata? }` describing the resources
|
|
93
|
+
the event affected.
|
|
94
|
+
- `success` — whether the operation succeeded.
|
|
95
|
+
- `data` — any additional structured data to attach to the event.
|
|
96
|
+
- `actor` — override the automatically-derived actor
|
|
97
|
+
(`{ sub, type, email, connection }`).
|
|
98
|
+
- `country`, `region`, `city` — override the caller's geolocation. These
|
|
99
|
+
default to the current request's location, so you normally don't need to set
|
|
100
|
+
them.
|
|
68
101
|
|
|
69
|
-
|
|
102
|
+
Audit logging is best-effort: `log()` never throws and never blocks or fails the
|
|
103
|
+
request, even if an event cannot be recorded.
|
|
70
104
|
|
|
71
|
-
Audit
|
|
72
|
-
retrieved by time span and can be filtered by `tenant`.
|
|
105
|
+
## Audit Log Event Shape
|
|
73
106
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
107
|
+
Every audit event follows the [CloudEvents](https://cloudevents.io/) 1.0
|
|
108
|
+
envelope. An automatically-emitted request event looks like this:
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"specversion": "1.0",
|
|
113
|
+
"id": "e6c8b1f2-2c3d-4a5b-9e10-1f2a3b4c5d6e",
|
|
114
|
+
"type": "com.zuplo.api.request",
|
|
115
|
+
"time": "2026-07-05T18:12:04.123Z",
|
|
116
|
+
"actorsub": "user|12356",
|
|
117
|
+
"actortype": "user",
|
|
118
|
+
"requestid": "b1a2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
119
|
+
"httpmethod": "GET",
|
|
120
|
+
"httpurl": "/customers/12345?expand=orders",
|
|
121
|
+
"httpstatus": 200,
|
|
122
|
+
"success": true,
|
|
123
|
+
"ipaddress": "203.0.113.7",
|
|
124
|
+
"useragent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...",
|
|
125
|
+
"country": "US",
|
|
126
|
+
"region": "Washington",
|
|
127
|
+
"city": "Seattle"
|
|
128
|
+
}
|
|
78
129
|
```
|
|
130
|
+
|
|
131
|
+
Custom events emitted via `log()` share the same envelope; the `type`,
|
|
132
|
+
`subject`, `resources`, `success`, and `data` fields come from the event you
|
|
133
|
+
pass, and the remaining fields are populated from the request context.
|
|
134
|
+
|
|
135
|
+
## Viewing Audit Logs
|
|
136
|
+
|
|
137
|
+
In-portal viewing and retrieval of audit logs is coming soon.
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
"type": "object",
|
|
5
5
|
"title": "Audit Logs",
|
|
6
6
|
"isDeprecated": false,
|
|
7
|
-
"isPaidAddOn":
|
|
8
|
-
"isEnterprise":
|
|
7
|
+
"isPaidAddOn": false,
|
|
8
|
+
"isEnterprise": true,
|
|
9
9
|
"isInternal": false,
|
|
10
10
|
"isBeta": false,
|
|
11
11
|
"isHidden": false,
|
|
12
12
|
"requiresAI": false,
|
|
13
13
|
"products": ["api-gateway"],
|
|
14
|
-
"description": "Capture detailed logs of requests for auditing purposes.",
|
|
14
|
+
"description": "Capture detailed logs of requests for auditing purposes. The policy emits one structured CloudEvent per request, and user code can emit additional events via the static `log()` method.",
|
|
15
15
|
"deprecatedMessage": "",
|
|
16
16
|
"required": ["handler"],
|
|
17
17
|
"properties": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"required": ["export", "module", "options"],
|
|
22
22
|
"properties": {
|
|
23
23
|
"export": {
|
|
24
|
-
"const": "
|
|
24
|
+
"const": "AuditLogInboundPolicy",
|
|
25
25
|
"description": "The name of the exported type"
|
|
26
26
|
},
|
|
27
27
|
"module": {
|
|
@@ -29,51 +29,71 @@
|
|
|
29
29
|
"description": "The module containing the policy"
|
|
30
30
|
},
|
|
31
31
|
"options": {
|
|
32
|
-
"title": "
|
|
32
|
+
"title": "AuditLogInboundPolicyOptions",
|
|
33
33
|
"type": "object",
|
|
34
|
-
"description": "The options for
|
|
34
|
+
"description": "The options for the Audit Log Inbound policy.",
|
|
35
35
|
"additionalProperties": false,
|
|
36
|
-
"required": [
|
|
36
|
+
"required": [],
|
|
37
37
|
"properties": {
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"description": "
|
|
38
|
+
"eventTypeBase": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"description": "Reverse-DNS prefix used for the auto-emitted request event type. Defaults to 'com.zuplo.api'.",
|
|
41
|
+
"default": "com.zuplo.api"
|
|
41
42
|
},
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"description": "
|
|
43
|
+
"bucketId": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"description": "Override the bucket id this policy emits events for. Defaults to the bucket id from the runtime environment."
|
|
45
46
|
},
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"description": "
|
|
47
|
+
"samplingRate": {
|
|
48
|
+
"type": "number",
|
|
49
|
+
"description": "Fraction of requests to capture, between 0 (none) and 1 (all). Defaults to 1.",
|
|
50
|
+
"default": 1,
|
|
51
|
+
"minimum": 0,
|
|
52
|
+
"maximum": 1
|
|
49
53
|
},
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"description": "
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
54
|
+
"include": {
|
|
55
|
+
"type": "object",
|
|
56
|
+
"description": "Controls which potentially-sensitive parts of each request are captured in the auto-emitted audit event. Disable fields to satisfy your own data-handling and PII policies. All are enabled by default.",
|
|
57
|
+
"additionalProperties": false,
|
|
58
|
+
"properties": {
|
|
59
|
+
"queryParams": {
|
|
60
|
+
"type": "boolean",
|
|
61
|
+
"default": true,
|
|
62
|
+
"description": "Include the request's query-string parameters in the logged URL. Disable if query strings may contain sensitive data."
|
|
63
|
+
},
|
|
64
|
+
"user": {
|
|
65
|
+
"type": "boolean",
|
|
66
|
+
"default": true,
|
|
67
|
+
"description": "Include the authenticated user / actor identity (subject, email, connection)."
|
|
68
|
+
},
|
|
69
|
+
"ipAddress": {
|
|
70
|
+
"type": "boolean",
|
|
71
|
+
"default": true,
|
|
72
|
+
"description": "Include the caller's IP address."
|
|
73
|
+
},
|
|
74
|
+
"geolocation": {
|
|
75
|
+
"type": "boolean",
|
|
76
|
+
"default": true,
|
|
77
|
+
"description": "Include the caller's geolocation (country, region, city)."
|
|
78
|
+
}
|
|
79
|
+
}
|
|
63
80
|
}
|
|
64
81
|
}
|
|
65
82
|
}
|
|
66
83
|
},
|
|
67
84
|
"examples": [
|
|
68
85
|
{
|
|
69
|
-
"export": "
|
|
86
|
+
"export": "AuditLogInboundPolicy",
|
|
70
87
|
"module": "$import(@zuplo/runtime)",
|
|
71
88
|
"options": {
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
89
|
+
"eventTypeBase": "com.zuplo.api",
|
|
90
|
+
"include": {
|
|
91
|
+
"geolocation": true,
|
|
92
|
+
"ipAddress": true,
|
|
93
|
+
"queryParams": true,
|
|
94
|
+
"user": true
|
|
95
|
+
},
|
|
96
|
+
"samplingRate": 1
|
|
77
97
|
}
|
|
78
98
|
}
|
|
79
99
|
]
|
|
@@ -50,6 +50,138 @@ gateway-managed upstream OAuth credentials. That order lets the token exchange
|
|
|
50
50
|
policy retry or replace a 401 response first; this policy then filters the final
|
|
51
51
|
upstream JSON-RPC response.
|
|
52
52
|
|
|
53
|
+
## Access control
|
|
54
|
+
|
|
55
|
+
`accessControl` narrows the curated catalog **per caller**. Its `mode` selects
|
|
56
|
+
the strategy:
|
|
57
|
+
|
|
58
|
+
- **`allPublic`** (default): no access control — every curated capability is
|
|
59
|
+
available to all callers. Setting `roles`/`groups`/`public` on a capability in
|
|
60
|
+
this mode is a configuration error (it would silently do nothing).
|
|
61
|
+
- **`rolesAndGroups`**: match the caller's roles and groups — read from
|
|
62
|
+
`request.user.data` (see below) — against each capability's markup.
|
|
63
|
+
- **`function`**: delegate to a custom resolver.
|
|
64
|
+
|
|
65
|
+
### Mode `rolesAndGroups`
|
|
66
|
+
|
|
67
|
+
Mark each capability with `roles` and/or `groups`, or `public: true`. **Every
|
|
68
|
+
capability must be classified** — one that sets neither (or both) is rejected at
|
|
69
|
+
load, so nothing is ever exposed by omission:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"options": {
|
|
74
|
+
"tools": [
|
|
75
|
+
{ "name": "search_invoices", "roles": ["billing", "admin"] },
|
|
76
|
+
{ "name": "create_invoice", "roles": ["admin"], "groups": ["finance"] },
|
|
77
|
+
{ "name": "list_products", "public": true }
|
|
78
|
+
],
|
|
79
|
+
"accessControl": { "mode": "rolesAndGroups" }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
A caller may use a capability if it is `public`, or if the caller matches any
|
|
85
|
+
listed role **or** any listed group (so `create_invoice` above is available to
|
|
86
|
+
admins or to the finance team). Anonymous callers get only `public`
|
|
87
|
+
capabilities. `roles`/`groups`/`public` are enforcement markup and are stripped
|
|
88
|
+
from downstream responses — use `_meta` for hints you want clients to see.
|
|
89
|
+
|
|
90
|
+
The caller's roles and groups come from `request.user.data` — the object an
|
|
91
|
+
upstream authentication policy sets for the authenticated caller. What lands
|
|
92
|
+
there depends on the policy: a JWT auth policy stores the token's claims, an API
|
|
93
|
+
key policy stores the key's metadata, and so on. This policy reads the `roles`
|
|
94
|
+
and `groups` properties by default. For example, behind a JWT auth policy a
|
|
95
|
+
token with these claims:
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"sub": "user_8f3a",
|
|
100
|
+
"roles": ["billing"],
|
|
101
|
+
"groups": ["sales"],
|
|
102
|
+
"iss": "https://acme.us.auth0.com/",
|
|
103
|
+
"aud": "https://mcp.acme.com",
|
|
104
|
+
"exp": 1893456000
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`request.user.data.roles` is `["billing"]` and `request.user.data.groups` is
|
|
109
|
+
`["sales"]`. Against the config above, this caller may use `search_invoices`
|
|
110
|
+
(matches the `billing` role) and the public `list_products`, but not
|
|
111
|
+
`create_invoice` (which needs the `admin` role or the `finance` group). A
|
|
112
|
+
request with no authenticated caller (no `request.user`) has no roles or groups,
|
|
113
|
+
so it sees only `public` capabilities.
|
|
114
|
+
|
|
115
|
+
Use `roleClaim`/`groupClaim` to read different `request.user.data` properties,
|
|
116
|
+
including a dot-path for nested values or a literal namespaced key:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"accessControl": { "mode": "rolesAndGroups", "groupClaim": "cognito:groups" }
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"accessControl": {
|
|
127
|
+
"mode": "rolesAndGroups",
|
|
128
|
+
"roleClaim": "realm_access.roles"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Mode `function`
|
|
134
|
+
|
|
135
|
+
For any other mapping — looking up entitlements from an external API, requiring
|
|
136
|
+
both a role AND a group, blocklists, etc. — set `mode: "function"` and point
|
|
137
|
+
`identifier` at a resolver (same `{ module, export }` shape as rate-limit). **It
|
|
138
|
+
fully replaces the built-in roles/groups matching.** It receives
|
|
139
|
+
`(request, context, options)` and returns the allowed identifiers; the result is
|
|
140
|
+
always clamped to the configured catalog, so a resolver can only narrow access.
|
|
141
|
+
A capability type left as passthrough (no static list) is not narrowed. A
|
|
142
|
+
resolver that throws or returns no value fails closed.
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
{
|
|
146
|
+
"accessControl": {
|
|
147
|
+
"mode": "function",
|
|
148
|
+
"identifier": {
|
|
149
|
+
"module": "$import(./modules/mcp-access-control)",
|
|
150
|
+
"export": "default"
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
// modules/mcp-access-control.ts
|
|
158
|
+
import { ZuploRequest, ZuploContext, environment } from "@zuplo/runtime";
|
|
159
|
+
import type {
|
|
160
|
+
McpCapabilityFilterInboundPolicyOptions,
|
|
161
|
+
AllowedCapabilities,
|
|
162
|
+
} from "@zuplo/runtime/mcp-gateway";
|
|
163
|
+
|
|
164
|
+
export default async function resolveCapabilities(
|
|
165
|
+
request: ZuploRequest,
|
|
166
|
+
context: ZuploContext,
|
|
167
|
+
options: McpCapabilityFilterInboundPolicyOptions
|
|
168
|
+
): Promise<AllowedCapabilities> {
|
|
169
|
+
const subject = request.user?.sub;
|
|
170
|
+
if (!subject) return { tools: [] }; // fail closed for anonymous
|
|
171
|
+
|
|
172
|
+
const res = await fetch(
|
|
173
|
+
`https://entitlements.example.com/users/${encodeURIComponent(subject)}`,
|
|
174
|
+
{ headers: { authorization: `Bearer ${environment.ENTITLEMENTS_API_KEY}` } }
|
|
175
|
+
);
|
|
176
|
+
if (!res.ok) {
|
|
177
|
+
context.log.warn(`entitlements lookup failed: ${res.status}`);
|
|
178
|
+
return { tools: [] };
|
|
179
|
+
}
|
|
180
|
+
const { allowedTools } = (await res.json()) as { allowedTools: string[] };
|
|
181
|
+
return { tools: allowedTools };
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
53
185
|
## Batch Requests
|
|
54
186
|
|
|
55
187
|
For JSON-RPC batch requests, list responses are filtered per response item when
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"isHidden": false,
|
|
12
12
|
"requiresAI": false,
|
|
13
13
|
"products": ["mcp-gateway"],
|
|
14
|
-
"description": "Curate MCP capabilities advertised and reachable through `McpProxyHandler`.\n\nValidation runs lazily inside the policy constructor, which the runtime caches per policy name. Misconfigured options therefore fail on first use with a customer-facing `ConfigurationError` instead of failing at module load.",
|
|
14
|
+
"description": "Curate MCP capabilities advertised and reachable through `McpProxyHandler`.\n\nValidation runs lazily inside the policy constructor, which the runtime caches per policy name. Misconfigured options therefore fail on first use with a customer-facing `ConfigurationError` instead of failing at module load.\n\nOptional per-caller access control (`accessControl`) narrows the curated catalog. See `claimsCapabilityResolver` for built-in role/group matching and `McpCapabilityResolver` for custom resolvers.",
|
|
15
15
|
"deprecatedMessage": "",
|
|
16
16
|
"required": ["handler"],
|
|
17
17
|
"properties": {
|
|
@@ -67,6 +67,28 @@
|
|
|
67
67
|
"type": "object",
|
|
68
68
|
"additionalProperties": true,
|
|
69
69
|
"description": "Arbitrary MCP \\_meta fields to expose downstream."
|
|
70
|
+
},
|
|
71
|
+
"roles": {
|
|
72
|
+
"type": "array",
|
|
73
|
+
"minItems": 1,
|
|
74
|
+
"items": {
|
|
75
|
+
"type": "string",
|
|
76
|
+
"minLength": 1
|
|
77
|
+
},
|
|
78
|
+
"description": "Access control: roles allowed to see and invoke this capability. Matched against the caller's roles on request.user.data (accessControl.roleClaim, default 'roles'). Used when accessControl.mode is 'rolesAndGroups'."
|
|
79
|
+
},
|
|
80
|
+
"groups": {
|
|
81
|
+
"type": "array",
|
|
82
|
+
"minItems": 1,
|
|
83
|
+
"items": {
|
|
84
|
+
"type": "string",
|
|
85
|
+
"minLength": 1
|
|
86
|
+
},
|
|
87
|
+
"description": "Access control: groups allowed to see and invoke this capability. Matched against the caller's groups on request.user.data (accessControl.groupClaim, default 'groups'). Used when accessControl.mode is 'rolesAndGroups'."
|
|
88
|
+
},
|
|
89
|
+
"public": {
|
|
90
|
+
"type": "boolean",
|
|
91
|
+
"description": "Access control: mark this capability as available to all callers. In mode 'rolesAndGroups' every capability must set roles/groups OR public: true (not both) — an unclassified capability is a configuration error, so nothing is ever exposed by omission."
|
|
70
92
|
}
|
|
71
93
|
}
|
|
72
94
|
}
|
|
@@ -100,6 +122,28 @@
|
|
|
100
122
|
"type": "object",
|
|
101
123
|
"additionalProperties": true,
|
|
102
124
|
"description": "Arbitrary MCP \\_meta fields to expose downstream."
|
|
125
|
+
},
|
|
126
|
+
"roles": {
|
|
127
|
+
"type": "array",
|
|
128
|
+
"minItems": 1,
|
|
129
|
+
"items": {
|
|
130
|
+
"type": "string",
|
|
131
|
+
"minLength": 1
|
|
132
|
+
},
|
|
133
|
+
"description": "Access control: roles allowed to see and invoke this capability. Matched against the caller's roles on request.user.data (accessControl.roleClaim, default 'roles'). Used when accessControl.mode is 'rolesAndGroups'."
|
|
134
|
+
},
|
|
135
|
+
"groups": {
|
|
136
|
+
"type": "array",
|
|
137
|
+
"minItems": 1,
|
|
138
|
+
"items": {
|
|
139
|
+
"type": "string",
|
|
140
|
+
"minLength": 1
|
|
141
|
+
},
|
|
142
|
+
"description": "Access control: groups allowed to see and invoke this capability. Matched against the caller's groups on request.user.data (accessControl.groupClaim, default 'groups'). Used when accessControl.mode is 'rolesAndGroups'."
|
|
143
|
+
},
|
|
144
|
+
"public": {
|
|
145
|
+
"type": "boolean",
|
|
146
|
+
"description": "Access control: mark this capability as available to all callers. In mode 'rolesAndGroups' every capability must set roles/groups OR public: true (not both) — an unclassified capability is a configuration error, so nothing is ever exposed by omission."
|
|
103
147
|
}
|
|
104
148
|
}
|
|
105
149
|
}
|
|
@@ -143,6 +187,28 @@
|
|
|
143
187
|
"type": "object",
|
|
144
188
|
"additionalProperties": true,
|
|
145
189
|
"description": "Arbitrary MCP \\_meta fields to expose downstream."
|
|
190
|
+
},
|
|
191
|
+
"roles": {
|
|
192
|
+
"type": "array",
|
|
193
|
+
"minItems": 1,
|
|
194
|
+
"items": {
|
|
195
|
+
"type": "string",
|
|
196
|
+
"minLength": 1
|
|
197
|
+
},
|
|
198
|
+
"description": "Access control: roles allowed to see and invoke this capability. Matched against the caller's roles on request.user.data (accessControl.roleClaim, default 'roles'). Used when accessControl.mode is 'rolesAndGroups'."
|
|
199
|
+
},
|
|
200
|
+
"groups": {
|
|
201
|
+
"type": "array",
|
|
202
|
+
"minItems": 1,
|
|
203
|
+
"items": {
|
|
204
|
+
"type": "string",
|
|
205
|
+
"minLength": 1
|
|
206
|
+
},
|
|
207
|
+
"description": "Access control: groups allowed to see and invoke this capability. Matched against the caller's groups on request.user.data (accessControl.groupClaim, default 'groups'). Used when accessControl.mode is 'rolesAndGroups'."
|
|
208
|
+
},
|
|
209
|
+
"public": {
|
|
210
|
+
"type": "boolean",
|
|
211
|
+
"description": "Access control: mark this capability as available to all callers. In mode 'rolesAndGroups' every capability must set roles/groups OR public: true (not both) — an unclassified capability is a configuration error, so nothing is ever exposed by omission."
|
|
146
212
|
}
|
|
147
213
|
}
|
|
148
214
|
}
|
|
@@ -186,11 +252,76 @@
|
|
|
186
252
|
"type": "object",
|
|
187
253
|
"additionalProperties": true,
|
|
188
254
|
"description": "Arbitrary MCP \\_meta fields to expose downstream."
|
|
255
|
+
},
|
|
256
|
+
"roles": {
|
|
257
|
+
"type": "array",
|
|
258
|
+
"minItems": 1,
|
|
259
|
+
"items": {
|
|
260
|
+
"type": "string",
|
|
261
|
+
"minLength": 1
|
|
262
|
+
},
|
|
263
|
+
"description": "Access control: roles allowed to see and invoke this capability. Matched against the caller's roles on request.user.data (accessControl.roleClaim, default 'roles'). Used when accessControl.mode is 'rolesAndGroups'."
|
|
264
|
+
},
|
|
265
|
+
"groups": {
|
|
266
|
+
"type": "array",
|
|
267
|
+
"minItems": 1,
|
|
268
|
+
"items": {
|
|
269
|
+
"type": "string",
|
|
270
|
+
"minLength": 1
|
|
271
|
+
},
|
|
272
|
+
"description": "Access control: groups allowed to see and invoke this capability. Matched against the caller's groups on request.user.data (accessControl.groupClaim, default 'groups'). Used when accessControl.mode is 'rolesAndGroups'."
|
|
273
|
+
},
|
|
274
|
+
"public": {
|
|
275
|
+
"type": "boolean",
|
|
276
|
+
"description": "Access control: mark this capability as available to all callers. In mode 'rolesAndGroups' every capability must set roles/groups OR public: true (not both) — an unclassified capability is a configuration error, so nothing is ever exposed by omission."
|
|
189
277
|
}
|
|
190
278
|
}
|
|
191
279
|
}
|
|
192
280
|
]
|
|
193
281
|
}
|
|
282
|
+
},
|
|
283
|
+
"accessControl": {
|
|
284
|
+
"type": "object",
|
|
285
|
+
"additionalProperties": false,
|
|
286
|
+
"description": "Optional per-caller access control. Defaults to mode 'allPublic' (no access control). Set mode to 'rolesAndGroups' to match the caller's role/group claims against each capability's markup (every capability must then set roles/groups or public: true), or 'function' to delegate to a custom resolver.",
|
|
287
|
+
"properties": {
|
|
288
|
+
"mode": {
|
|
289
|
+
"type": "string",
|
|
290
|
+
"enum": ["allPublic", "rolesAndGroups", "function"],
|
|
291
|
+
"default": "allPublic",
|
|
292
|
+
"description": "Access-control strategy. 'allPublic' (default): no access control; roles/groups/public markup is not allowed. 'rolesAndGroups': match the caller's role and group claims against each capability's markup. 'function': delegate to the resolver named by 'identifier'."
|
|
293
|
+
},
|
|
294
|
+
"roleClaim": {
|
|
295
|
+
"type": "string",
|
|
296
|
+
"minLength": 1,
|
|
297
|
+
"default": "roles",
|
|
298
|
+
"description": "Property on request.user.data holding the caller's roles (commonly a JWT 'roles' claim, but request.user.data is set by whichever auth policy ran — JWT, API key, etc.), matched against each capability's 'roles'. Supports a dot-path for nested values, e.g. 'realm\\_access.roles'. Used in mode 'rolesAndGroups'. Defaults to 'roles'."
|
|
299
|
+
},
|
|
300
|
+
"groupClaim": {
|
|
301
|
+
"type": "string",
|
|
302
|
+
"minLength": 1,
|
|
303
|
+
"default": "groups",
|
|
304
|
+
"description": "Property on request.user.data holding the caller's groups (commonly a JWT 'groups' claim, but request.user.data is set by whichever auth policy ran — JWT, API key, etc.), matched against each capability's 'groups'. Supports a dot-path for nested values, e.g. 'cognito:groups'. Used in mode 'rolesAndGroups'. Defaults to 'groups'."
|
|
305
|
+
},
|
|
306
|
+
"identifier": {
|
|
307
|
+
"type": "object",
|
|
308
|
+
"additionalProperties": false,
|
|
309
|
+
"required": ["module", "export"],
|
|
310
|
+
"description": "Custom capability resolver, required when mode is 'function'. It fully replaces built-in roles/groups matching.",
|
|
311
|
+
"properties": {
|
|
312
|
+
"module": {
|
|
313
|
+
"type": "string",
|
|
314
|
+
"minLength": 1,
|
|
315
|
+
"description": "Module to load the resolver from, e.g. $import(./modules/mcp-access-control)."
|
|
316
|
+
},
|
|
317
|
+
"export": {
|
|
318
|
+
"type": "string",
|
|
319
|
+
"minLength": 1,
|
|
320
|
+
"description": "Named export of the resolver function, e.g. 'default'."
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
194
325
|
}
|
|
195
326
|
}
|
|
196
327
|
}
|
|
@@ -200,6 +331,12 @@
|
|
|
200
331
|
"export": "McpCapabilityFilterInboundPolicy",
|
|
201
332
|
"module": "$import(@zuplo/runtime)",
|
|
202
333
|
"options": {
|
|
334
|
+
"accessControl": {
|
|
335
|
+
"groupClaim": "groups",
|
|
336
|
+
"identifier": {},
|
|
337
|
+
"mode": "allPublic",
|
|
338
|
+
"roleClaim": "roles"
|
|
339
|
+
},
|
|
203
340
|
"prompts": [],
|
|
204
341
|
"resourceTemplates": [],
|
|
205
342
|
"resources": [],
|
|
@@ -59,6 +59,32 @@ import { MonetizationInboundPolicy } from "@zuplo/runtime";
|
|
|
59
59
|
const meters = MonetizationInboundPolicy.getMeters(context);
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
+
### Flush (send) runtime meters mid-request
|
|
63
|
+
|
|
64
|
+
Use `flushMeters` to send accumulated runtime meters immediately and reset their
|
|
65
|
+
counts. This is useful for long-running requests where you want to report
|
|
66
|
+
partial usage before the response is sent (for example, after each chunk of work
|
|
67
|
+
in a streaming handler).
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import { MonetizationInboundPolicy } from "@zuplo/runtime";
|
|
71
|
+
|
|
72
|
+
MonetizationInboundPolicy.addMeters(context, { input_tokens: 500 });
|
|
73
|
+
await MonetizationInboundPolicy.flushMeters(context);
|
|
74
|
+
|
|
75
|
+
// Runtime meters are cleared before the backend send begins so a concurrent
|
|
76
|
+
// final hook cannot double-count them. Meters are restored if the send fails.
|
|
77
|
+
MonetizationInboundPolicy.addMeters(context, { input_tokens: 300 });
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Static `options.meters` are not included in `flushMeters`; they are still sent
|
|
81
|
+
in the final response hook.
|
|
82
|
+
|
|
83
|
+
Note: `flushMeters` performs entitlement validation and will throw if called
|
|
84
|
+
before the monetization policy runs on the request or if you send meters that
|
|
85
|
+
aren’t present/accessible on the subscription. Wrap in `try/catch` if you want
|
|
86
|
+
to continue the request when a flush fails.
|
|
87
|
+
|
|
62
88
|
## Subscription data (for customization)
|
|
63
89
|
|
|
64
90
|
The monetization policy validates the API key and attaches the **subscription**
|
|
@@ -163,4 +189,7 @@ For a meter key like `api` with `options.meters.api = 1`:
|
|
|
163
189
|
|
|
164
190
|
- `setMeters` replaces current request meter increments.
|
|
165
191
|
- `addMeters` accumulates values across multiple calls.
|
|
192
|
+
- `flushMeters` sends runtime meters immediately, clears them before the backend
|
|
193
|
+
send (to avoid duplicate sends if the final hook runs concurrently), and
|
|
194
|
+
restores them if the send fails.
|
|
166
195
|
- Entitlements are validated before usage is sent.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zuplo",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.72.0",
|
|
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.
|
|
23
|
-
"@zuplo/core": "6.
|
|
24
|
-
"@zuplo/runtime": "6.
|
|
22
|
+
"@zuplo/cli": "6.72.0",
|
|
23
|
+
"@zuplo/core": "6.72.0",
|
|
24
|
+
"@zuplo/runtime": "6.72.0",
|
|
25
25
|
"@zuplo/test": "1.4.0"
|
|
26
26
|
}
|
|
27
27
|
}
|