zuplo 6.72.2 → 6.72.7

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.
Files changed (61) hide show
  1. package/docs/analytics/access-and-entitlements.md +27 -34
  2. package/docs/analytics/overview.md +7 -6
  3. package/docs/analytics/reference/url-parameters.md +2 -10
  4. package/docs/analytics/shared-controls.md +3 -10
  5. package/docs/analytics/tabs/agents.md +3 -3
  6. package/docs/analytics/tabs/requests.md +2 -4
  7. package/docs/articles/accounts/audit-logs.mdx +4 -4
  8. package/docs/articles/audit-logging.mdx +65 -0
  9. package/docs/articles/audit-logs.mdx +222 -0
  10. package/docs/articles/connect-to-aws-alb-with-mtls.mdx +89 -13
  11. package/docs/articles/{custom-audit-log-policy.mdx → custom-audit-logs.mdx} +26 -22
  12. package/docs/articles/graphql-security.mdx +2 -1
  13. package/docs/articles/graphql.mdx +8 -0
  14. package/docs/articles/opentelemetry.mdx +7 -5
  15. package/docs/articles/securing-backend-mtls.mdx +92 -3
  16. package/docs/articles/troubleshooting-slow-responses.mdx +4 -3
  17. package/docs/articles/troubleshooting.md +3 -1
  18. package/docs/policies/_index.md +3 -0
  19. package/docs/policies/audit-log-inbound/doc.md +16 -11
  20. package/docs/policies/authzen-inbound/schema.json +1 -1
  21. package/docs/policies/axiomatics-authz-inbound/schema.json +1 -1
  22. package/docs/policies/clerk-jwt-auth-inbound/schema.json +3 -3
  23. package/docs/policies/mcp-auth0-oauth-inbound/schema.json +8 -5
  24. package/docs/policies/mcp-clerk-oauth-inbound/schema.json +78 -0
  25. package/docs/policies/mcp-cognito-oauth-inbound/schema.json +78 -0
  26. package/docs/policies/mcp-entra-oauth-inbound/schema.json +78 -0
  27. package/docs/policies/mcp-google-oauth-inbound/schema.json +78 -0
  28. package/docs/policies/mcp-keycloak-oauth-inbound/schema.json +78 -0
  29. package/docs/policies/mcp-logto-oauth-inbound/schema.json +78 -0
  30. package/docs/policies/mcp-oauth-inbound/schema.json +8 -5
  31. package/docs/policies/mcp-okta-oauth-inbound/schema.json +78 -0
  32. package/docs/policies/mcp-onelogin-oauth-inbound/schema.json +78 -0
  33. package/docs/policies/mcp-ping-oauth-inbound/schema.json +78 -0
  34. package/docs/policies/mcp-token-exchange-inbound/doc.md +72 -4
  35. package/docs/policies/mcp-token-exchange-inbound/intro.md +5 -4
  36. package/docs/policies/mcp-token-exchange-inbound/schema.json +277 -2
  37. package/docs/policies/mcp-workos-oauth-inbound/schema.json +78 -0
  38. package/docs/policies/mtls-auth-inbound/intro.md +52 -5
  39. package/docs/policies/okta-fga-authz-inbound/schema.json +1 -1
  40. package/docs/policies/open-id-jwt-auth-inbound/schema.json +3 -3
  41. package/docs/policies/openfga-authz-inbound/schema.json +1 -1
  42. package/docs/policies/propel-auth-jwt-inbound/schema.json +3 -3
  43. package/docs/policies/require-user-claims-inbound/doc.md +120 -0
  44. package/docs/policies/require-user-claims-inbound/intro.md +9 -0
  45. package/docs/policies/require-user-claims-inbound/schema.json +1323 -0
  46. package/docs/policies/semantic-cache-inbound/schema.json +1 -1
  47. package/docs/policies/upstream-aws-federated-auth-inbound/doc.md +78 -0
  48. package/docs/policies/upstream-aws-federated-auth-inbound/intro.md +20 -0
  49. package/docs/policies/upstream-aws-federated-auth-inbound/schema.json +98 -0
  50. package/docs/policies/upstream-aws-service-auth-inbound/doc.md +89 -0
  51. package/docs/policies/upstream-aws-service-auth-inbound/intro.md +19 -0
  52. package/docs/policies/upstream-aws-service-auth-inbound/schema.json +113 -0
  53. package/docs/policies/upstream-azure-ad-service-auth-inbound/schema.json +4 -4
  54. package/docs/policies/upstream-gcp-federated-auth-inbound/doc.md +2 -2
  55. package/docs/policies/upstream-gcp-federated-auth-inbound/schema.json +5 -5
  56. package/docs/policies/upstream-gcp-service-auth-inbound/doc.md +3 -3
  57. package/docs/policies/upstream-gcp-service-auth-inbound/schema.json +3 -3
  58. package/docs/policies/upstream-zuplo-jwt-auth-inbound/schema.json +19 -0
  59. package/docs/programmable-api/overview.mdx +0 -7
  60. package/package.json +4 -4
  61. package/docs/programmable-api/audit-log.mdx +0 -74
@@ -7,56 +7,49 @@ sidebar_label: "Access & Entitlements"
7
7
 
8
8
  - Confirm whether your account can see analytics.
9
9
  - Find out how many days of history you have access to.
10
- - Understand the trial banner or the demo mode link.
10
+ - Understand the trial banner.
11
11
 
12
- ## Plan requirements
12
+ ## Availability
13
13
 
14
- Analytics must be enabled on your account. Without it, the Analytics page shows
15
- an upsell view with a **Contact Sales** call-to-action and no charts.
14
+ Analytics is included on every plan. Your plan determines how much history you
15
+ can query see [Data retention](#data-retention). If the Analytics page shows
16
+ an upsell view with a **Contact Sales** call-to-action instead of charts, your
17
+ account has analytics disabled — contact your Zuplo account team.
16
18
 
17
19
  ## Free trial
18
20
 
19
- New accounts with analytics enabled get an automatic free trial. The trial:
21
+ Every new account gets an automatic 14-day observability trial. During the
22
+ trial:
20
23
 
21
- - Runs for the same number of days as your account's retention window.
22
- - Shows a banner across the top of the Analytics page: "You're on a {N}-day
23
- preview of Analytics, {N} days left."
24
- - Includes two call-to-actions: **View demo →** (loads the dashboard with sample
25
- data) and **Contact sales**.
24
+ - You get longer history windows for analytics, logs, and traces than your plan
25
+ includes. Accounts on self-serve plans get the Builder/Business windows (7
26
+ days of analytics, 1 day of logs and traces); Enterprise trial accounts get
27
+ the Enterprise windows. See [Data retention](#data-retention).
28
+ - A banner appears across the top of the Observability tabs: "You're on a 14-day
29
+ trial of Zuplo Observability — {N} days left", with a **Contact sales** link.
26
30
 
27
- Accounts on the legacy analytics version are not eligible for the trial. They
28
- continue to use the previous experience.
31
+ When the trial ends, your plan's own history windows apply.
29
32
 
30
- :::note
31
-
32
- The trial banner notes that the charts may look sparse if your account hasn't
33
- yet generated much traffic. Use **View demo →** to see what a fully populated
34
- dashboard looks like.
33
+ ## Data retention
35
34
 
36
- :::
35
+ Each account has history windows for analytics, logs, and traces. The windows
36
+ depend on your plan:
37
37
 
38
- ## Data retention
38
+ | Plan | Analytics history | Log & trace history |
39
+ | ------------------ | ----------------- | ------------------- |
40
+ | Free | 1 day | Live (15 minutes) |
41
+ | Builder / Business | 7 days | 1 day |
42
+ | Enterprise | 30 days | 3 days |
39
43
 
40
- Each account has an analytics history window measured in days. The window
41
- controls:
44
+ The analytics window controls:
42
45
 
43
46
  - How far back you can scroll using the time-range picker.
44
47
  - Which presets in the picker are available. Presets longer than your window are
45
- locked with an **Upgrade for [preset]** tooltip.
48
+ locked and show an **Upgrade** badge; hover one for details.
46
49
  - The maximum start and end values when you pick a custom range.
47
50
 
48
- If you need a longer window, contact your Zuplo account team.
49
-
50
- ## Demo mode
51
-
52
- Append `?demo=true` to the Analytics URL, or click **View demo →** in the trial
53
- banner, to switch into demo mode. In demo mode:
54
-
55
- - Charts and tables are populated with synthetic sample data.
56
- - A persistent banner reads: "You're viewing the Analytics demo with sample
57
- data. Your real analytics aren't shown here."
58
-
59
- Remove the `demo` parameter from the URL to return to your real data.
51
+ If you need a longer window, contact your Zuplo account team — custom retention
52
+ is available on Enterprise plans.
60
53
 
61
54
  ## Scope: account vs project
62
55
 
@@ -32,7 +32,7 @@ Analytics lives in the **Observability** tab of the Zuplo Portal, alongside
32
32
  ## What's in this section
33
33
 
34
34
  - [Access and entitlements](./access-and-entitlements.md): plans, free trial,
35
- demo mode, retention.
35
+ retention.
36
36
  - [Shared controls](./shared-controls.md): time range, filters, environment
37
37
  selector, banners, URL state.
38
38
  - Sections:
@@ -56,12 +56,13 @@ You'll see a subset of sections depending on your plan and project setup:
56
56
 
57
57
  | Section | When it appears |
58
58
  | --------- | ---------------------------------------------------------- |
59
- | Requests | All accounts with analytics enabled. |
59
+ | Requests | All accounts. |
60
60
  | Origins | The project uses managed-edge origins. |
61
- | Consumers | All accounts with analytics enabled. |
62
- | Agents | All accounts with analytics enabled. |
61
+ | Consumers | All accounts. |
62
+ | Agents | All accounts. |
63
63
  | MCP | The project type is **standard** and the project uses MCP. |
64
64
  | GraphQL | The project proxies a GraphQL API. |
65
65
 
66
- If you don't see Analytics at all, your account likely doesn't have analytics
67
- enabled. See [Access and entitlements](./access-and-entitlements.md).
66
+ Analytics is included on every plan. If you don't see Analytics at all, your
67
+ account may have it disabled — see
68
+ [Access and entitlements](./access-and-entitlements.md).
@@ -8,7 +8,7 @@ view.
8
8
 
9
9
  ## When to use this
10
10
 
11
- - Build a permalink to a specific time window, filter set, or demo view.
11
+ - Build a permalink to a specific time window or filter set.
12
12
  - Embed an Analytics link in a runbook, postmortem, or dashboard.
13
13
  - Understand what each query parameter does.
14
14
 
@@ -16,11 +16,9 @@ view.
16
16
 
17
17
  | Parameter | Example | Effect |
18
18
  | -------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------- |
19
- | `time` | `?time=7d` | Apply a preset. Values: `1h`, `6h`, `24h`, `3d`, `7d`, `14d`, `28d`, `60d`, `90d`. |
19
+ | `time` | `?time=7d` | Apply a preset. Values: `1h`, `6h`, `24h`, `3d`, `7d`, `14d`, `28d`, `30d`, `60d`, `90d`. |
20
20
  | `start`, `end` | `?start=2026-05-01T00:00:00Z&end=2026-05-15T00:00:00Z` | Custom range as ISO-8601 datetimes. Overrides `time` when both are present. |
21
21
  | `filter` | `?filter=httpStatus:class:5xx` | Add a filter as `<field>:<matchmode>:<value>`. Repeat the parameter for multiple filters. |
22
- | `demo` | `?demo=true` | Demo mode (sample data instead of your real analytics). |
23
- | `preview` | `?preview=1` | Legacy preview mode. |
24
22
 
25
23
  ## Match modes for `filter`
26
24
 
@@ -48,12 +46,6 @@ Custom range with two filters:
48
46
  ?start=2026-05-01T00:00:00Z&end=2026-05-08T00:00:00Z&filter=country:equals:US&filter=httpMethod:equals:POST
49
47
  ```
50
48
 
51
- Open the demo:
52
-
53
- ```
54
- ?demo=true
55
- ```
56
-
57
49
  ## Sharing
58
50
 
59
51
  The recipient sees the same view, provided they have access to the project or
@@ -18,7 +18,7 @@ State persists to the URL so you can share or bookmark any view.
18
18
  The time range picker controls every chart, table, and KPI in the active
19
19
  section.
20
20
 
21
- **Presets.** Last 1h, 6h, 24h, 3d, 7d, 14d, 28d, 60d, 90d.
21
+ **Presets.** Last 1h, 6h, 24h, 3d, 7d, 14d, 28d, 30d, 60d, 90d.
22
22
 
23
23
  **Custom range.** Use the datetime-local inputs for **Start** and **End**. Both
24
24
  fields are clamped to your account's retention window.
@@ -80,8 +80,6 @@ There's no separate share button.
80
80
  | `time` | `?time=7d` | Apply a preset. |
81
81
  | `start`, `end` | `?start=2026-05-01T00:00:00Z&end=2026-05-15T00:00:00Z` | Custom range. Overrides `time`. |
82
82
  | `filter` | `?filter=httpStatus:class:5xx` | Add a filter. Repeat the parameter for multiple values. |
83
- | `demo` | `?demo=true` | Demo mode (sample data). |
84
- | `preview` | `?preview=1` | Legacy preview mode. |
85
83
 
86
84
  See [URL parameters](./reference/url-parameters.md) for the full reference.
87
85
 
@@ -94,13 +92,8 @@ a refetch.
94
92
 
95
93
  ## Banners
96
94
 
97
- Banners appear at the top of the page in this priority order:
98
-
99
- 1. **Preview banner**: when `preview=1` is set. Indicates legacy preview mode.
100
- 2. **Demo banner**: when `demo=true` is set. Reminds you sample data is shown
101
- instead of your real analytics.
102
- 3. **Trial banner**: for new accounts with analytics. Shows days remaining and
103
- offers **View demo →** and **Contact Sales**.
95
+ During the 14-day observability trial, a banner appears at the top of the page
96
+ showing the days remaining, with a **Contact sales** link.
104
97
 
105
98
  ## Loading and empty states
106
99
 
@@ -74,9 +74,9 @@ The filter bar applies. `originHost` is not applicable here. See
74
74
  ## Troubleshooting
75
75
 
76
76
  **The Agents section is empty.** Either no classified agents called your gateway
77
- in the window, or your retention window doesn't yet include any agent traffic.
78
- Try the demo with **View demo →** in the trial banner to see what a populated
79
- view looks like. See [Access and entitlements](../access-and-entitlements.md).
77
+ in the window, or your
78
+ [retention window](../access-and-entitlements.md#data-retention) doesn't yet
79
+ include any agent traffic.
80
80
 
81
81
  **I see a known agent in my logs but not here.** The classifier is conservative;
82
82
  it labels traffic that clearly matches a known agent fingerprint. Generic SDK
@@ -91,7 +91,5 @@ not include any geolocated requests.
91
91
  that breakdown. Top-10 plus 50 covers up to 60 distinct values; beyond that,
92
92
  narrow the time range or add a filter.
93
93
 
94
- **My charts look sparse.** If your account is new, the trial banner across the
95
- top calls this out. Click **View demo →** in the banner to see what a fully
96
- populated dashboard looks like. See
97
- [Access and entitlements](../access-and-entitlements.md).
94
+ **My charts look sparse.** New accounts may not have generated much traffic yet.
95
+ Charts fill in as requests flow through your gateway.
@@ -1,5 +1,5 @@
1
1
  ---
2
- title: Audit Logs
2
+ title: Account Audit Logs
3
3
  sidebar_label: Audit Logs
4
4
  ---
5
5
 
@@ -211,8 +211,8 @@ exceed 30 days. The start date must be before the end date.
211
211
 
212
212
  :::tip
213
213
 
214
- Account audit logs track administrative activities. To log API request and
215
- response data flowing through the gateway, see the
216
- [Audit Log Plugin](../../programmable-api/audit-log.mdx).
214
+ Account audit logs track administrative activities in your Zuplo account. To
215
+ record an audit trail of the requests flowing through your gateway, see
216
+ [Audit Logging](../audit-logging.mdx).
217
217
 
218
218
  :::
@@ -0,0 +1,65 @@
1
+ ---
2
+ title: Audit Logging
3
+ sidebar_label: Overview
4
+ ---
5
+
6
+ Audit logs answer the question "who did what through your API, and when?" —
7
+ providing a structured, searchable record of every request and business event
8
+ that flows through your gateway. This page explains why audit logging matters
9
+ and the options Zuplo gives you for capturing an audit trail.
10
+
11
+ ## Why audit logging?
12
+
13
+ Audit logging plays a critical role in API security: it lets you detect and
14
+ investigate issues such as unauthorized access or permission elevation, and
15
+ reconstruct exactly what happened to a resource after the fact. It's also a hard
16
+ requirement for many compliance certifications (SOC 2, ISO 27001, HIPAA, PCI
17
+ DSS) and a common buying criterion for enterprise customers of your API.
18
+
19
+ Unlike request logs — which are optimized for debugging and operational
20
+ visibility — audit logs are structured business records: each event identifies
21
+ an actor, an action, and the resources affected, in a stable format you can
22
+ retain, search, and hand to an auditor.
23
+
24
+ ## Options for audit logging
25
+
26
+ Zuplo supports two approaches, depending on where you want your audit trail to
27
+ live:
28
+
29
+ 1. **[Zuplo Audit Logs](./audit-logs.mdx)** — the recommended approach for most
30
+ APIs. Add a single policy and Zuplo records, stores, and indexes a structured
31
+ audit event for every request. You can search the events in the Zuplo Portal,
32
+ query them through the Zuplo API, and export them to a SIEM. No code or
33
+ infrastructure required.
34
+ 2. **[Custom audit logs](./custom-audit-logs.mdx)** — write a small custom
35
+ policy that sends audit events to any external audit provider (for example,
36
+ WorkOS Audit Logs). Full control over the event format and destination.
37
+
38
+ | | Zuplo Audit Logs | Custom audit logs |
39
+ | ------------ | ------------------------- | --------------------- |
40
+ | Setup | Add a policy | Write policy code |
41
+ | Storage | Managed by Zuplo | Your provider |
42
+ | Viewing | Zuplo Portal + Zuplo API | Your provider's tools |
43
+ | Availability | Enterprise (free to test) | All plans |
44
+
45
+ Zuplo Audit Logs is an enterprise feature, but anyone can use it for free for
46
+ development and testing purposes.
47
+
48
+ ## Gateway audit logs versus account audit logs
49
+
50
+ Zuplo has two separate audit trails — the options above all concern the first:
51
+
52
+ - **Gateway audit logs** record traffic through _your API_: requests from your
53
+ API's consumers and custom events emitted by your gateway code.
54
+ - **[Account audit logs](./accounts/audit-logs.mdx)** record administrative
55
+ activity in _your Zuplo account_: project changes, team member management,
56
+ deployments, and other portal and API operations.
57
+
58
+ ## Next steps
59
+
60
+ - [Zuplo Audit Logs](./audit-logs.mdx) — enable the built-in feature and start
61
+ recording events
62
+ - [Custom audit logs](./custom-audit-logs.mdx) — send audit events to an
63
+ external provider
64
+ - [Audit Logs policy reference](../policies/audit-log-inbound.mdx) — all
65
+ configuration options and the full event shape
@@ -0,0 +1,222 @@
1
+ ---
2
+ title: Zuplo Audit Logs
3
+ sidebar_label: Zuplo Audit Logs
4
+ ---
5
+
6
+ Zuplo's Audit Logs feature gives you a complete
7
+ [audit trail](./audit-logging.mdx) of your API with a single policy: the gateway
8
+ records a structured event for every request, stores the events for you, and
9
+ makes them available in the Zuplo Portal and through the Zuplo API.
10
+
11
+ <EnterpriseFeature name="Audit Logs" />
12
+
13
+ ## How it works
14
+
15
+ <Diagram height="h-56">
16
+ <DiagramNode id="client">Client</DiagramNode>
17
+ <DiagramGroup id="zuplo" label="Zuplo">
18
+ <DiagramNode id="gateway" variant="zuplo">
19
+ Gateway (Audit Logs policy)
20
+ </DiagramNode>
21
+ <DiagramNode id="store">Audit log storage</DiagramNode>
22
+ </DiagramGroup>
23
+ <DiagramNode id="consumers">Portal / API / SIEM</DiagramNode>
24
+ <DiagramEdge from="client" to="gateway" />
25
+ <DiagramEdge from="gateway" to="store" label="events" />
26
+ <DiagramEdge from="store" to="consumers" />
27
+ </Diagram>
28
+
29
+ Add the [Audit Logs policy](../policies/audit-log-inbound.mdx) to any route and
30
+ the gateway records one structured audit event per request — who made the
31
+ request, what they called, and the outcome. You can also emit your own custom
32
+ events (for example, "account deleted") from handlers and custom policies. Every
33
+ event follows the [CloudEvents](https://cloudevents.io/) 1.0 format.
34
+
35
+ Zuplo stores the events for you, grouped by environment — production, preview,
36
+ and working copy environments each write to their own bucket. You don't need a
37
+ database, logging service, or any other infrastructure of your own.
38
+
39
+ ## Recording audit events
40
+
41
+ ### Automatic request events
42
+
43
+ Add the policy to the routes you want audited. Typically that's any route that
44
+ modifies data, though depending on your API you may want it on sensitive read
45
+ operations as well (for example, retrieving a secret).
46
+
47
+ ```json title="config/policies.json"
48
+ {
49
+ "name": "audit-logs",
50
+ "policyType": "audit-log-inbound",
51
+ "handler": {
52
+ "export": "AuditLogInboundPolicy",
53
+ "module": "$import(@zuplo/runtime)"
54
+ }
55
+ }
56
+ ```
57
+
58
+ Each request through the policy produces an event of type
59
+ `com.zuplo.api.request` capturing the actor, HTTP method and path, response
60
+ status, caller IP address, and geolocation.
61
+
62
+ Audit events can contain personal or sensitive data. The policy's `include`
63
+ options let you turn off individual fields (query parameters, user identity, IP
64
+ address, geolocation) to satisfy your own data-handling and personally
65
+ identifiable information (PII) policies, and `samplingRate` lets you capture
66
+ only a fraction of requests on high-volume routes. See the
67
+ [policy reference](../policies/audit-log-inbound.mdx) for all options.
68
+
69
+ ### Custom events
70
+
71
+ Beyond the automatic per-request event, record your own domain events from a
72
+ handler or custom policy with the static `AuditLogInboundPolicy.log()` method.
73
+ Only `type` is required — Zuplo fills in the actor, timestamp, request ID, and
74
+ other context fields automatically from the request:
75
+
76
+ ```ts title="modules/handlers.ts"
77
+ import {
78
+ AuditLogInboundPolicy,
79
+ ZuploContext,
80
+ ZuploRequest,
81
+ } from "@zuplo/runtime";
82
+
83
+ export async function deleteAccount(
84
+ request: ZuploRequest,
85
+ context: ZuploContext,
86
+ ) {
87
+ const accountId = request.params.accountId;
88
+
89
+ // ...perform the delete...
90
+
91
+ AuditLogInboundPolicy.log(context, {
92
+ type: "com.acme.account.deleted",
93
+ subject: `account|${accountId}`,
94
+ resources: [{ type: "account", id: accountId }],
95
+ success: true,
96
+ });
97
+
98
+ return new Response(null, { status: 204 });
99
+ }
100
+ ```
101
+
102
+ Audit logging is best-effort by design: `log()` never throws and never blocks or
103
+ fails the request, even if an event can't be recorded.
104
+
105
+ ## Viewing audit logs in the portal
106
+
107
+ To browse and search your audit logs, open your project's
108
+ [**Services**](https://portal.zuplo.com/+/account/project/services) page in the
109
+ Zuplo Portal and select the **Audit Logs** tile for the environment you want to
110
+ inspect.
111
+
112
+ The events view lets you:
113
+
114
+ - Filter events by time range, event type, actor, and subject
115
+ - See the top event types and most active actors for the selected window
116
+ - Select any event to inspect the full event payload
117
+
118
+ ## Querying audit logs with the API
119
+
120
+ Everything in the portal is backed by the Zuplo API, so you can query audit logs
121
+ programmatically — for scripting, custom dashboards, or exporting to other
122
+ systems. Authenticate with a [Zuplo API key](./accounts/zuplo-api-keys.mdx) and
123
+ query events by bucket name. Your bucket name is shown on the **Services** page
124
+ in the portal (it's the same bucket used by the API Key Service).
125
+
126
+ ```bash
127
+ curl "https://dev.zuplo.com/v1/audit-logs/$BUCKET_NAME/events?type=com.zuplo.api.request&startDate=2026-07-01T00:00:00Z&endDate=2026-07-07T00:00:00Z" \
128
+ -H "Authorization: Bearer $ZUPLO_API_KEY"
129
+ ```
130
+
131
+ ```json
132
+ {
133
+ "data": [
134
+ {
135
+ "specversion": "1.0",
136
+ "id": "e6c8b1f2-2c3d-4a5b-9e10-1f2a3b4c5d6e",
137
+ "type": "com.zuplo.api.request",
138
+ "time": "2026-07-05T18:12:04.123Z",
139
+ "actorsub": "user|12356",
140
+ "actortype": "user",
141
+ "requestid": "b1a2c3d4-e5f6-7890-abcd-ef1234567890",
142
+ "httpmethod": "GET",
143
+ "httpurl": "/customers/12345?expand=orders",
144
+ "httpstatus": 200,
145
+ "success": true,
146
+ "ipaddress": "203.0.113.7",
147
+ "useragent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...",
148
+ "country": "US",
149
+ "region": "Washington",
150
+ "city": "Seattle"
151
+ }
152
+ ],
153
+ "pagination": { "limit": 20, "offset": 0, "total": 1, "hasMore": false }
154
+ }
155
+ ```
156
+
157
+ Two endpoints are available:
158
+
159
+ - **Query events** — `GET /v1/audit-logs/{bucketId}/events` returns matching
160
+ events, filterable by `type`, `actorSub`, `subject`, and time range.
161
+ - **Aggregated stats** — `GET /v1/audit-logs/{bucketId}/stats` returns top-N
162
+ event counts grouped by event type or actor.
163
+
164
+ The `bucketId` path parameter accepts either the bucket ID or the bucket name.
165
+ Each query is limited to a 30-day window between `startDate` and `endDate`. See
166
+ the [Audit Logs API reference](../api/audit-logs) for all parameters and
167
+ response schemas.
168
+
169
+ ## Exporting audit logs to a SIEM
170
+
171
+ Many organizations centralize audit trails in a SIEM (Splunk, Microsoft
172
+ Sentinel, Datadog Cloud SIEM, and so on) to correlate API activity with other
173
+ security signals, drive alerting, and meet long-term retention requirements. The
174
+ events API is designed for this: run a scheduled job that pulls new events since
175
+ its last checkpoint and forwards them to your SIEM.
176
+
177
+ ```ts
178
+ const BASE_URL = "https://dev.zuplo.com/v1/audit-logs";
179
+
180
+ // Run on a schedule (e.g. every 5 minutes). `startDate` is the checkpoint
181
+ // saved by the previous run; `endDate` is the current time.
182
+ async function exportAuditLogs(
183
+ bucketName: string,
184
+ startDate: string,
185
+ endDate: string,
186
+ ) {
187
+ let offset = 0;
188
+ let hasMore = true;
189
+ while (hasMore) {
190
+ const url = new URL(`${BASE_URL}/${bucketName}/events`);
191
+ url.searchParams.set("startDate", startDate);
192
+ url.searchParams.set("endDate", endDate);
193
+ url.searchParams.set("limit", "100");
194
+ url.searchParams.set("offset", String(offset));
195
+ const response = await fetch(url, {
196
+ headers: { Authorization: `Bearer ${process.env.ZUPLO_API_KEY}` },
197
+ });
198
+ const { data, pagination } = await response.json();
199
+ await sendToSiem(data); // e.g. Splunk HEC, Datadog Logs API, S3
200
+ offset += data.length;
201
+ hasMore = pagination.hasMore;
202
+ }
203
+ }
204
+ ```
205
+
206
+ Because every event is a CloudEvents document, most SIEMs can ingest the payload
207
+ directly without transformation.
208
+
209
+ Alternatively, if you'd rather send audit events to an external audit provider
210
+ directly from the gateway instead of storing them in Zuplo, see
211
+ [Custom Audit Logs](./custom-audit-logs.mdx).
212
+
213
+ ## Related resources
214
+
215
+ - [Audit logging overview](./audit-logging.mdx) — why audit logging matters and
216
+ how the options compare
217
+ - [Audit Logs policy reference](../policies/audit-log-inbound.mdx) — all
218
+ configuration options, custom event fields, and the full event shape
219
+ - [Audit Logs API reference](../api/audit-logs) — query events and stats
220
+ programmatically
221
+ - [Account audit logs](./accounts/audit-logs.mdx) — the audit trail of your
222
+ Zuplo account itself
@@ -20,9 +20,9 @@ present an X.509 certificate that chains to a Certificate Authority (CA) in the
20
20
  ALB's trust store. Zuplo presents that client certificate on each outbound
21
21
  request, and the ALB rejects anything that can't.
22
22
 
23
- This guide covers the Zuplo side of that connection: uploading a client
24
- certificate and presenting it on requests to the ALB. It does **not** cover
25
- configuring the ALB itself — for that, follow the AWS documentation linked in
23
+ This guide covers uploading a client certificate to Zuplo and presenting it on
24
+ requests to the ALB. It does **not** cover configuring the ALB itself — for
25
+ that, follow the AWS documentation linked in
26
26
  [Configure the ALB](#1-configure-the-alb).
27
27
 
28
28
  ## How it works
@@ -64,10 +64,13 @@ For background on the gateway-to-origin direction in general, see
64
64
 
65
65
  Before you begin, you need:
66
66
 
67
- - A client certificate and private key (PEM-encoded) issued by a CA. The same CA
68
- must be uploaded to the ALB's trust store.
67
+ - A PEM-encoded client certificate (`client-cert.pem`), matching private key
68
+ (`client-key.pem`), and public CA certificate or bundle (`client-ca.pem`). The
69
+ same CA must be in the ALB's trust store.
69
70
  - An AWS Application Load Balancer with an HTTPS listener you can configure for
70
71
  mTLS.
72
+ - The AWS CLI installed and authenticated with permission to inspect the ALB's
73
+ listener and trust store.
71
74
  - The [Zuplo CLI](../cli/overview.mdx) installed and authenticated.
72
75
 
73
76
  ## 1/ Configure the ALB
@@ -93,7 +96,79 @@ edge.
93
96
 
94
97
  :::
95
98
 
96
- ## 2/ Upload your client certificate to Zuplo
99
+ ## 2/ Validate the certificate and ALB trust
100
+
101
+ Confirm that the listener uses verify mode and capture its trust store ARN:
102
+
103
+ ```bash
104
+ aws elbv2 describe-listeners \
105
+ --listener-arns <HTTPS_LISTENER_ARN> \
106
+ --query 'Listeners[0].MutualAuthentication' \
107
+ --output json
108
+
109
+ TRUST_STORE_ARN=$(aws elbv2 describe-listeners \
110
+ --listener-arns <HTTPS_LISTENER_ARN> \
111
+ --query 'Listeners[0].MutualAuthentication.TrustStoreArn' \
112
+ --output text)
113
+
114
+ aws elbv2 describe-trust-stores \
115
+ --trust-store-arns "$TRUST_STORE_ARN" \
116
+ --query 'TrustStores[0].{Name:Name,Status:Status,CACertificates:NumberOfCaCertificates}' \
117
+ --output table
118
+ ```
119
+
120
+ The listener mode should be `verify`, and the trust store status should be
121
+ `ACTIVE`.
122
+
123
+ Inspect the client certificate and verify its path against the CA certificate:
124
+
125
+ ```bash
126
+ openssl x509 \
127
+ -in client-cert.pem \
128
+ -noout \
129
+ -subject \
130
+ -issuer \
131
+ -dates \
132
+ -purpose
133
+
134
+ openssl verify \
135
+ -verbose \
136
+ -purpose sslclient \
137
+ -x509_strict \
138
+ -show_chain \
139
+ -CAfile client-ca.pem \
140
+ client-cert.pem
141
+ ```
142
+
143
+ `SSL client` should report `Yes`, and verification should return
144
+ `client-cert.pem: OK`. Also confirm that the certificate and private key match:
145
+
146
+ ```bash
147
+ CERT_KEY_HASH=$(openssl x509 -in client-cert.pem -noout -pubkey | openssl sha256)
148
+ PRIVATE_KEY_HASH=$(openssl pkey -in client-key.pem -pubout | openssl sha256)
149
+
150
+ test "$CERT_KEY_HASH" = "$PRIVATE_KEY_HASH" &&
151
+ echo "Certificate and private key match" ||
152
+ echo "ERROR: Certificate and private key do not match"
153
+ ```
154
+
155
+ For the strongest offline check, run `openssl verify` with the exact CA bundle
156
+ uploaded to the ALB trust store instead of `client-ca.pem`.
157
+
158
+ If the ALB is reachable, test the TLS handshake directly:
159
+
160
+ ```bash
161
+ curl --verbose \
162
+ --cert client-cert.pem \
163
+ --key client-key.pem \
164
+ "https://<ALB_HOSTNAME>/<TEST_PATH>"
165
+ ```
166
+
167
+ Use a hostname covered by the ALB's server certificate. Any HTTP response,
168
+ including a `401`, `403`, or `404`, shows that the TLS handshake completed; a
169
+ client certificate failure occurs before an HTTP response is produced.
170
+
171
+ ## 3/ Upload your client certificate to Zuplo
97
172
 
98
173
  Use the Zuplo CLI to upload the client certificate and private key to your
99
174
  project. The CA that issued this certificate must already be in the ALB's trust
@@ -119,7 +194,7 @@ when you create the certificate.
119
194
 
120
195
  :::
121
196
 
122
- ## 3/ Present the certificate on requests to the ALB
197
+ ## 4/ Present the certificate on requests to the ALB
123
198
 
124
199
  Reference the uploaded certificate by name when the gateway forwards requests to
125
200
  the ALB. Use the ALB's DNS name (or a custom domain pointed at it) as the
@@ -162,7 +237,7 @@ export default async function (request: ZuploRequest, context: ZuploContext) {
162
237
  }
163
238
  ```
164
239
 
165
- ## 4/ Use environment variables across environments
240
+ ## 5/ Use environment variables across environments
166
241
 
167
242
  To use a different certificate per environment, store the certificate name in an
168
243
  [environment variable](./environment-variables.mdx) and reference it with the
@@ -226,11 +301,12 @@ received — usually a TLS handshake problem. Confirm that:
226
301
 
227
302
  ### The ALB rejects the certificate
228
303
 
229
- If the handshake completes but the ALB returns a `403`, the certificate is being
230
- presented but not trusted. Re-check the ALB trust store and confirm the listener
231
- is in **verify** mode, not passthrough. If your client certificate is issued by
232
- an intermediate CA, make sure the trust store contains the full chain back to
233
- the root.
304
+ If the TLS handshake fails, re-check the ALB trust store and confirm the
305
+ listener is in **verify** mode, not passthrough. If your client certificate is
306
+ issued by an intermediate CA, make sure the trust store contains the CA chain
307
+ required to validate the certificate. ALB connection logs can identify errors
308
+ such as `ClientCertUntrusted`, `ClientCertPurposeInvalid`, `ClientCertExpired`,
309
+ or `ClientCertNotYetValid`.
234
310
 
235
311
  ### No certificate appears to be sent
236
312