zuplo 6.71.7 → 6.71.9
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/local-development-troubleshooting.mdx +10 -0
- package/docs/articles/monetization/dynamic-metering.md +105 -0
- package/docs/articles/monetization/index.mdx +22 -19
- package/docs/articles/monetization/meters.mdx +4 -4
- package/docs/articles/monetization/monetization-policy.md +33 -55
- package/docs/articles/monetization/programmatic-monetization.md +254 -0
- package/docs/articles/monetization/subscription-data.md +153 -0
- package/docs/articles/openapi-string-format-validation-warnings.mdx +142 -0
- package/docs/articles/troubleshooting-deployments-and-git-sync.mdx +222 -0
- package/package.json +4 -4
|
@@ -36,6 +36,16 @@ certificates, you can add the following script:
|
|
|
36
36
|
}
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
## "unknown format ... ignored" warnings
|
|
40
|
+
|
|
41
|
+
When the development server prints warnings like
|
|
42
|
+
`unknown format "date-time" ignored in schema at path "…"`, your OpenAPI schema
|
|
43
|
+
uses a `format` keyword that the validator does not actively check. These
|
|
44
|
+
warnings are safe to ignore — they do not stop the server or change validation
|
|
45
|
+
behavior. See
|
|
46
|
+
[OpenAPI Format Validation Warnings](./openapi-string-format-validation-warnings.mdx)
|
|
47
|
+
for details.
|
|
48
|
+
|
|
39
49
|
## Updating the Zuplo CLI
|
|
40
50
|
|
|
41
51
|
To update the CLI, run the following command in your project directory.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Dynamic Metering
|
|
3
|
+
sidebar_label: Dynamic Metering
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Most routes meter a fixed amount per request through the policy's
|
|
7
|
+
[`meters`](./monetization-policy.md#meters) option. For variable-cost endpoints
|
|
8
|
+
— an AI endpoint billed by tokens returned, a search billed by records matched —
|
|
9
|
+
the amount isn't known until the backend responds. Set meter values from code at
|
|
10
|
+
runtime with the `MonetizationInboundPolicy` static methods.
|
|
11
|
+
|
|
12
|
+
## The runtime metering methods
|
|
13
|
+
|
|
14
|
+
| Method | What it does |
|
|
15
|
+
| ---------------------------- | ----------------------------------------------------------------------- |
|
|
16
|
+
| `setMeters(context, meters)` | Replaces the runtime meter map, overriding matching static keys |
|
|
17
|
+
| `addMeters(context, meters)` | Adds to the runtime meter map, accumulating with static and prior calls |
|
|
18
|
+
| `getMeters(context)` | Returns the current runtime meter map |
|
|
19
|
+
|
|
20
|
+
Call them from a custom policy or handler. Because the values usually come from
|
|
21
|
+
the response, the most common place is a custom outbound policy.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import {
|
|
25
|
+
MonetizationInboundPolicy,
|
|
26
|
+
ZuploContext,
|
|
27
|
+
ZuploRequest,
|
|
28
|
+
} from "@zuplo/runtime";
|
|
29
|
+
|
|
30
|
+
// In a custom outbound policy, set meters based on the response
|
|
31
|
+
export default async function (
|
|
32
|
+
response: Response,
|
|
33
|
+
request: ZuploRequest,
|
|
34
|
+
context: ZuploContext,
|
|
35
|
+
) {
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
return response;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Reading the body consumes it, so rebuild the response afterward
|
|
41
|
+
const body = (await response.json()) as {
|
|
42
|
+
usage?: { total_tokens?: number };
|
|
43
|
+
};
|
|
44
|
+
const tokens = body.usage?.total_tokens ?? 0;
|
|
45
|
+
|
|
46
|
+
MonetizationInboundPolicy.setMeters(context, { tokens_used: tokens });
|
|
47
|
+
|
|
48
|
+
return new Response(JSON.stringify(body), {
|
|
49
|
+
status: response.status,
|
|
50
|
+
headers: response.headers,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Use `addMeters` to add to existing meter values rather than replacing them:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
MonetizationInboundPolicy.addMeters(context, {
|
|
59
|
+
api_credits: creditsConsumed,
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Read the current runtime meter values at any point:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
const meters = MonetizationInboundPolicy.getMeters(context);
|
|
67
|
+
// { tokens_used: 150 }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## How meter values are merged
|
|
71
|
+
|
|
72
|
+
The final metering hook combines static and runtime values before sending usage:
|
|
73
|
+
|
|
74
|
+
- `options.meters` provides the static base values.
|
|
75
|
+
- `setMeters` replaces the runtime meter map, overriding matching static keys.
|
|
76
|
+
- `addMeters` accumulates into the runtime meter map, then combines additively
|
|
77
|
+
with static values.
|
|
78
|
+
- When both the static and runtime maps are empty, the policy skips metering.
|
|
79
|
+
|
|
80
|
+
For a meter key like `api` with `options.meters.api = 1`:
|
|
81
|
+
|
|
82
|
+
- `setMeters(context, { api: 50 })` sends `api: 50` (replaces the static value).
|
|
83
|
+
- `addMeters(context, { api: 50 })` sends `api: 51` (adds to the static value).
|
|
84
|
+
|
|
85
|
+
The policy reports usage only for the status codes set by
|
|
86
|
+
[`meterOnStatusCodes`](./monetization-policy.md#meteronstatuscodes), so a failed
|
|
87
|
+
backend response costs the caller nothing.
|
|
88
|
+
|
|
89
|
+
## Enforcing quotas on runtime meters
|
|
90
|
+
|
|
91
|
+
Runtime meters set from an outbound policy run _after_ the response, so they
|
|
92
|
+
can't block the current request on their own. To enforce a quota on a value you
|
|
93
|
+
meter at runtime, declare the meter statically with a value of `0` — the policy
|
|
94
|
+
checks the entitlement up front without double-counting. See
|
|
95
|
+
[Block on a response-derived meter](./programmatic-monetization.md#block-on-a-response-derived-meter).
|
|
96
|
+
|
|
97
|
+
## Next steps
|
|
98
|
+
|
|
99
|
+
- [Programmatic Monetization](./programmatic-monetization.md) — gate operations
|
|
100
|
+
by plan and enforce quotas on runtime meters.
|
|
101
|
+
- [Reading Subscription Data](./subscription-data.md) — inspect the plan and
|
|
102
|
+
entitlements in code.
|
|
103
|
+
- [Monetization Policy Reference](./monetization-policy.md) — every policy
|
|
104
|
+
configuration option.
|
|
105
|
+
- [Meters](./meters.mdx) — defining the meters you increment.
|
|
@@ -85,22 +85,25 @@ pricing, wire up Stripe, and configure your gateway to enforce quotas.
|
|
|
85
85
|
|
|
86
86
|
## Documentation map
|
|
87
87
|
|
|
88
|
-
| Document
|
|
89
|
-
|
|
|
90
|
-
| [Quickstart](./monetization/quickstart.md)
|
|
91
|
-
| [Meters](./monetization/meters.mdx)
|
|
92
|
-
| [Features](./monetization/features.mdx)
|
|
93
|
-
| [Plans](./monetization/plans.mdx)
|
|
94
|
-
| [Rate Cards](./monetization/rate-cards.mdx)
|
|
95
|
-
| [Pricing Models](./monetization/pricing-models.mdx)
|
|
96
|
-
| [Billing Models Guide](./monetization/billing-models.md)
|
|
97
|
-
| [Stripe Integration](./monetization/stripe-integration.md)
|
|
98
|
-
| [Developer Portal Setup](./monetization/developer-portal.md)
|
|
99
|
-
| [Monetization Policy Reference](./monetization/monetization-policy.md)
|
|
100
|
-
| [
|
|
101
|
-
| [
|
|
102
|
-
| [
|
|
103
|
-
| [
|
|
104
|
-
| [
|
|
105
|
-
| [
|
|
106
|
-
| [
|
|
88
|
+
| Document | What it covers |
|
|
89
|
+
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- |
|
|
90
|
+
| [Quickstart](./monetization/quickstart.md) | End-to-end setup in 30 minutes |
|
|
91
|
+
| [Meters](./monetization/meters.mdx) | How meters track usage dimensions |
|
|
92
|
+
| [Features](./monetization/features.mdx) | Connecting meters to your product catalog |
|
|
93
|
+
| [Plans](./monetization/plans.mdx) | Plan structure, phases, lifecycle, and publishing |
|
|
94
|
+
| [Rate Cards](./monetization/rate-cards.mdx) | Pricing and entitlements within plans |
|
|
95
|
+
| [Pricing Models](./monetization/pricing-models.mdx) | Flat, per-unit, tiered, volume, and package pricing |
|
|
96
|
+
| [Billing Models Guide](./monetization/billing-models.md) | Choosing the right pricing strategy for your business |
|
|
97
|
+
| [Stripe Integration](./monetization/stripe-integration.md) | Connecting Stripe and managing payments |
|
|
98
|
+
| [Developer Portal Setup](./monetization/developer-portal.md) | Configuring the self-serve portal for your customers |
|
|
99
|
+
| [Monetization Policy Reference](./monetization/monetization-policy.md) | Configuring the `MonetizationInboundPolicy` per-route |
|
|
100
|
+
| [Subscription Data](./monetization/subscription-data.md) | Reading the active subscription — plan, entitlements, payment status — in code |
|
|
101
|
+
| [Dynamic Metering](./monetization/dynamic-metering.md) | Setting meter values at runtime with `setMeters`, `addMeters`, and `getMeters` |
|
|
102
|
+
| [Programmatic Monetization](./monetization/programmatic-monetization.md) | Gating operations by plan and metering from the response in code |
|
|
103
|
+
| [API Access](./monetization/api-access.mdx) | Authentication, buckets, bucket configuration, Stripe setup APIs, and reference links |
|
|
104
|
+
| [Subscription Lifecycle](./monetization/subscription-lifecycle.md) | Managing trials, upgrades, downgrades, and cancellations |
|
|
105
|
+
| [Private Plans](./monetization/private-plans.md) | Invite-only plans for specific users |
|
|
106
|
+
| [Tax Collection](./monetization/tax-collection.md) | Enabling VAT, sales tax, or GST via Stripe Tax |
|
|
107
|
+
| [Going to Production](./monetization/going-to-production.mdx) | Pre-launch checklist, Stripe live mode, and known limitations |
|
|
108
|
+
| [Plan Examples](./monetization/plan-examples.mdx) | Real-world plan configurations |
|
|
109
|
+
| [Troubleshooting](./monetization/troubleshooting.md) | Common issues, debugging, and FAQ |
|
|
@@ -83,10 +83,10 @@ Track token consumption for AI applications:
|
|
|
83
83
|
The meter aggregates events from the gateway; the per-request quantity comes
|
|
84
84
|
from the `MonetizationInboundPolicy`. Set a fixed cost per request in the
|
|
85
85
|
policy's `meters` option, or call
|
|
86
|
-
[`MonetizationInboundPolicy.setMeters`](./
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
[`MonetizationInboundPolicy.setMeters`](./dynamic-metering.md) from a custom
|
|
87
|
+
outbound policy to report a value derived from the response — for example, the
|
|
88
|
+
actual token count an LLM returned. An event for a request that consumed 50
|
|
89
|
+
tokens looks like this:
|
|
90
90
|
|
|
91
91
|
```json
|
|
92
92
|
{
|
|
@@ -8,6 +8,16 @@ every request to a protected route, authenticates the API key, checks the
|
|
|
8
8
|
customer's subscription and payment status, enforces quota, meters the request,
|
|
9
9
|
and allows or blocks access.
|
|
10
10
|
|
|
11
|
+
:::tip
|
|
12
|
+
|
|
13
|
+
Working with monetization in code? See
|
|
14
|
+
[Reading Subscription Data](./subscription-data.md) to inspect the plan and
|
|
15
|
+
entitlements, [Dynamic Metering](./dynamic-metering.md) to set meter values at
|
|
16
|
+
runtime, and [Programmatic Monetization](./programmatic-monetization.md) to gate
|
|
17
|
+
operations by plan.
|
|
18
|
+
|
|
19
|
+
:::
|
|
20
|
+
|
|
11
21
|
## Basic configuration
|
|
12
22
|
|
|
13
23
|
Add the policy to your `policies.json`:
|
|
@@ -68,7 +78,7 @@ If `meters` is omitted, the policy still authenticates the API key and validates
|
|
|
68
78
|
the subscription's payment status, but no usage is recorded. If `meters` is
|
|
69
79
|
provided, it must contain at least one entry — an empty object throws a
|
|
70
80
|
configuration error. To track usage at runtime instead of from static config,
|
|
71
|
-
see [Dynamic
|
|
81
|
+
see the [Dynamic Metering](./dynamic-metering.md) guide.
|
|
72
82
|
|
|
73
83
|
```json
|
|
74
84
|
// Increment the api_requests meter by 1 per request
|
|
@@ -152,6 +162,13 @@ below it:
|
|
|
152
162
|
|
|
153
163
|
Set the value to `0` to block requests immediately when payment is overdue.
|
|
154
164
|
|
|
165
|
+
:::tip
|
|
166
|
+
|
|
167
|
+
Read the subscription's plan, entitlements, and payment status in your own code
|
|
168
|
+
with [`getSubscriptionData`](./subscription-data.md).
|
|
169
|
+
|
|
170
|
+
:::
|
|
171
|
+
|
|
155
172
|
## Multiple policies for different routes
|
|
156
173
|
|
|
157
174
|
Different routes can have different metering configurations. Define multiple
|
|
@@ -210,60 +227,12 @@ Apply each to the appropriate routes:
|
|
|
210
227
|
|
|
211
228
|
## Dynamic metering
|
|
212
229
|
|
|
213
|
-
For
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
```typescript
|
|
220
|
-
import { MonetizationInboundPolicy } from "@zuplo/runtime";
|
|
221
|
-
|
|
222
|
-
// In a custom outbound policy, set meters based on the response
|
|
223
|
-
export default async function meterTokens(response, request, context) {
|
|
224
|
-
if (response.ok) {
|
|
225
|
-
const body = await response.json();
|
|
226
|
-
const tokens = body.usage?.total_tokens ?? 0;
|
|
227
|
-
|
|
228
|
-
MonetizationInboundPolicy.setMeters(context, {
|
|
229
|
-
tokens_used: tokens,
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
return response;
|
|
233
|
-
}
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
You can also use `addMeters` to add to existing meter values rather than
|
|
237
|
-
replacing them:
|
|
238
|
-
|
|
239
|
-
```typescript
|
|
240
|
-
MonetizationInboundPolicy.addMeters(context, {
|
|
241
|
-
api_credits: creditsConsumed,
|
|
242
|
-
});
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
You can also read the current runtime meter values at any point:
|
|
246
|
-
|
|
247
|
-
```typescript
|
|
248
|
-
const meters = MonetizationInboundPolicy.getMeters(context);
|
|
249
|
-
// { tokens_used: 150 }
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
### How meter values are merged
|
|
253
|
-
|
|
254
|
-
The final metering hook combines static and runtime values before usage is sent:
|
|
255
|
-
|
|
256
|
-
- `options.meters` provides the static base values
|
|
257
|
-
- `setMeters` replaces the current runtime meter map, overriding matching static
|
|
258
|
-
keys
|
|
259
|
-
- `addMeters` accumulates into the runtime meter map, then combines additively
|
|
260
|
-
with static values
|
|
261
|
-
- If both static and runtime maps are empty, metering is skipped
|
|
262
|
-
|
|
263
|
-
For a meter key like `api` with `options.meters.api = 1`:
|
|
264
|
-
|
|
265
|
-
- `setMeters(context, { api: 50 })` sends `api: 50` (replaces static value)
|
|
266
|
-
- `addMeters(context, { api: 50 })` sends `api: 51` (adds to static value)
|
|
230
|
+
For variable-cost endpoints — billing by tokens returned, records processed, or
|
|
231
|
+
any value computed at runtime — set meter values from code with `setMeters`,
|
|
232
|
+
`addMeters`, and `getMeters` instead of static config. See the
|
|
233
|
+
[Dynamic Metering](./dynamic-metering.md) guide for the full API and merge
|
|
234
|
+
rules, and [Programmatic Monetization](./programmatic-monetization.md) for
|
|
235
|
+
gating operations and enforcing quotas on runtime meters.
|
|
267
236
|
|
|
268
237
|
## Error responses
|
|
269
238
|
|
|
@@ -320,3 +289,12 @@ If you still want per-second or per-minute rate limiting on top of monthly
|
|
|
320
289
|
quotas, add a standalone rate-limiting policy after the monetization policy.
|
|
321
290
|
These serve different purposes: monetization enforces billing quotas, while rate
|
|
322
291
|
limiting protects against traffic spikes.
|
|
292
|
+
|
|
293
|
+
## Related guides
|
|
294
|
+
|
|
295
|
+
- [Reading Subscription Data](./subscription-data.md) — inspect the plan,
|
|
296
|
+
entitlements, and payment status in code.
|
|
297
|
+
- [Dynamic Metering](./dynamic-metering.md) — set meter values at runtime with
|
|
298
|
+
`setMeters`, `addMeters`, and `getMeters`.
|
|
299
|
+
- [Programmatic Monetization](./programmatic-monetization.md) — gate operations
|
|
300
|
+
by plan and enforce quotas on response-derived meters.
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Programmatic Monetization
|
|
3
|
+
sidebar_label: Programmatic Monetization
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Pricing rules often depend on _who's_ calling or _what_ your API returns. A few
|
|
7
|
+
examples:
|
|
8
|
+
|
|
9
|
+
- Restrict bulk export to the Enterprise plan.
|
|
10
|
+
- Offer advanced search only to plans that include it.
|
|
11
|
+
- Bill an AI endpoint by the tokens it returns, and a search endpoint by the
|
|
12
|
+
rows it matches — not a flat amount per call.
|
|
13
|
+
|
|
14
|
+
You can enforce rules and meter usage declaratively with
|
|
15
|
+
[features and entitlements](./features.mdx) on your plans, in code with custom
|
|
16
|
+
policies, or both together. This guide covers the code path, for decisions that
|
|
17
|
+
need runtime logic. It shows two techniques, then combines them:
|
|
18
|
+
|
|
19
|
+
- **[Gate operations by plan](#gate-operations-by-plan)** — read the caller's
|
|
20
|
+
subscription and allow or block the request based on their plan or
|
|
21
|
+
entitlements.
|
|
22
|
+
- **[Meter from the response](#meter-from-the-response)** — compute usage from
|
|
23
|
+
the response body, report it, and enforce a quota on it.
|
|
24
|
+
|
|
25
|
+
Both build on the [`MonetizationInboundPolicy`](./monetization-policy.md): the
|
|
26
|
+
[subscription data](./subscription-data.md) it exposes and its
|
|
27
|
+
[dynamic metering](./dynamic-metering.md) methods. Add that policy to your
|
|
28
|
+
monetized routes first.
|
|
29
|
+
|
|
30
|
+
## Gate operations by plan
|
|
31
|
+
|
|
32
|
+
To make an operation available on some plans but not others, read the
|
|
33
|
+
subscription in a custom code inbound policy and block the request when the plan
|
|
34
|
+
doesn't qualify. Place the policy _after_ `monetization-inbound` so the
|
|
35
|
+
subscription data exists on the context.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
// modules/plan-gate.ts
|
|
39
|
+
import {
|
|
40
|
+
MonetizationInboundPolicy,
|
|
41
|
+
HttpProblems,
|
|
42
|
+
ZuploContext,
|
|
43
|
+
ZuploRequest,
|
|
44
|
+
} from "@zuplo/runtime";
|
|
45
|
+
|
|
46
|
+
export default async function (request: ZuploRequest, context: ZuploContext) {
|
|
47
|
+
const subscription = MonetizationInboundPolicy.getSubscriptionData(context);
|
|
48
|
+
|
|
49
|
+
if (!subscription) {
|
|
50
|
+
return HttpProblems.forbidden(request, context, {
|
|
51
|
+
detail: "No active subscription",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Bulk export is an Enterprise-only operation
|
|
56
|
+
if (subscription.plan.key !== "enterprise") {
|
|
57
|
+
return HttpProblems.forbidden(request, context, {
|
|
58
|
+
detail: "Bulk export requires the Enterprise plan",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return request;
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
To gate on a specific feature instead of the whole plan, check its entitlement:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
const advancedSearch = subscription.entitlements["advanced_search"];
|
|
70
|
+
if (!advancedSearch?.hasAccess) {
|
|
71
|
+
return HttpProblems.forbidden(request, context, {
|
|
72
|
+
detail: "Your plan does not include advanced search",
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Register the policy and apply it after `monetization-inbound` on the routes you
|
|
78
|
+
want to protect:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
// config/policies.json
|
|
82
|
+
{
|
|
83
|
+
"name": "plan-gate",
|
|
84
|
+
"policyType": "custom-code-inbound",
|
|
85
|
+
"handler": {
|
|
86
|
+
"export": "default",
|
|
87
|
+
"module": "$import(./modules/plan-gate)"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
// On the route
|
|
94
|
+
{
|
|
95
|
+
"x-zuplo-route": {
|
|
96
|
+
"policies": {
|
|
97
|
+
"inbound": ["monetization-inbound", "plan-gate"]
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Meter from the response
|
|
104
|
+
|
|
105
|
+
For variable-cost endpoints, meter a request by something you only know once the
|
|
106
|
+
backend responds — the number of records returned, items processed, or tokens
|
|
107
|
+
generated. Compute the value in a custom code outbound policy and report it with
|
|
108
|
+
`MonetizationInboundPolicy.addMeters`.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
// modules/count-records.ts
|
|
112
|
+
import {
|
|
113
|
+
MonetizationInboundPolicy,
|
|
114
|
+
ZuploContext,
|
|
115
|
+
ZuploRequest,
|
|
116
|
+
} from "@zuplo/runtime";
|
|
117
|
+
|
|
118
|
+
export default async function (
|
|
119
|
+
response: Response,
|
|
120
|
+
request: ZuploRequest,
|
|
121
|
+
context: ZuploContext,
|
|
122
|
+
) {
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
return response;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Reading the body consumes it, so rebuild the response afterward
|
|
128
|
+
const data = (await response.json()) as { records: unknown[] };
|
|
129
|
+
const recordCount = data.records?.length ?? 0;
|
|
130
|
+
|
|
131
|
+
MonetizationInboundPolicy.addMeters(context, { records: recordCount });
|
|
132
|
+
|
|
133
|
+
return new Response(JSON.stringify(data), {
|
|
134
|
+
status: response.status,
|
|
135
|
+
headers: response.headers,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`addMeters` accumulates with any static `meters` and earlier calls; use
|
|
141
|
+
`setMeters` to replace the runtime value instead. The policy sends the combined
|
|
142
|
+
total once the response goes out, and only for the configured status codes. See
|
|
143
|
+
[Dynamic Metering](./dynamic-metering.md) for the full API and merge rules.
|
|
144
|
+
|
|
145
|
+
## Block on a response-derived meter
|
|
146
|
+
|
|
147
|
+
`addMeters` runs in an outbound policy — it sees the response only after the
|
|
148
|
+
handler returns. By then it's too late to block the current request, and because
|
|
149
|
+
the meter never appeared in the policy's static config, the inbound quota check
|
|
150
|
+
never ran for it. A caller who is already over their limit still gets through.
|
|
151
|
+
|
|
152
|
+
To enforce the quota, declare the meter in the policy's `meters` option with a
|
|
153
|
+
value of **`0`**:
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
// config/policies.json
|
|
157
|
+
{
|
|
158
|
+
"name": "monetization-inbound",
|
|
159
|
+
"policyType": "monetization-inbound",
|
|
160
|
+
"handler": {
|
|
161
|
+
"export": "MonetizationInboundPolicy",
|
|
162
|
+
"module": "$import(@zuplo/runtime)",
|
|
163
|
+
"options": {
|
|
164
|
+
"meters": { "records": 0 }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
This does two things:
|
|
171
|
+
|
|
172
|
+
1. **Enforces the quota up front.** At request time the policy checks the
|
|
173
|
+
`records` entitlement and returns `403 Forbidden` when the balance has run
|
|
174
|
+
out — before the request reaches your backend.
|
|
175
|
+
2. **Avoids double-counting.** The static `0` contributes nothing to the total.
|
|
176
|
+
Runtime values add to the static base (`0 + n = n`), so `addMeters` reports
|
|
177
|
+
the exact amount to bill.
|
|
178
|
+
|
|
179
|
+
:::note
|
|
180
|
+
|
|
181
|
+
The quota check runs at the start of each request and lets through any caller
|
|
182
|
+
who still has balance; the policy charges usage after the response. So a request
|
|
183
|
+
whose cost exceeds the remaining balance still completes, the balance goes
|
|
184
|
+
negative, and the policy blocks the next request. An increment of `1` enforces
|
|
185
|
+
the quota exactly — a caller overshoots only when a single request meters more
|
|
186
|
+
than `1` against a partial balance.
|
|
187
|
+
|
|
188
|
+
:::
|
|
189
|
+
|
|
190
|
+
## Putting it together
|
|
191
|
+
|
|
192
|
+
A complete setup for a metered, plan-gated, response-counted route:
|
|
193
|
+
|
|
194
|
+
```json
|
|
195
|
+
// config/policies.json
|
|
196
|
+
{
|
|
197
|
+
"policies": [
|
|
198
|
+
{
|
|
199
|
+
"name": "monetization-inbound",
|
|
200
|
+
"policyType": "monetization-inbound",
|
|
201
|
+
"handler": {
|
|
202
|
+
"export": "MonetizationInboundPolicy",
|
|
203
|
+
"module": "$import(@zuplo/runtime)",
|
|
204
|
+
"options": {
|
|
205
|
+
"meters": { "records": 0 }
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"name": "plan-gate",
|
|
211
|
+
"policyType": "custom-code-inbound",
|
|
212
|
+
"handler": {
|
|
213
|
+
"export": "default",
|
|
214
|
+
"module": "$import(./modules/plan-gate)"
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"name": "count-records",
|
|
219
|
+
"policyType": "custom-code-outbound",
|
|
220
|
+
"handler": {
|
|
221
|
+
"export": "default",
|
|
222
|
+
"module": "$import(./modules/count-records)"
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
]
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
```json
|
|
230
|
+
// On the route
|
|
231
|
+
{
|
|
232
|
+
"x-zuplo-route": {
|
|
233
|
+
"policies": {
|
|
234
|
+
"inbound": ["monetization-inbound", "plan-gate"],
|
|
235
|
+
"outbound": ["count-records"]
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
The request flows through the monetization policy (authenticates, checks the
|
|
242
|
+
`records` quota, blocks if the quota has run out), then the plan gate (allows
|
|
243
|
+
the operation only on qualifying plans), then your handler, then the outbound
|
|
244
|
+
policy (counts the records and reports them with `addMeters`).
|
|
245
|
+
|
|
246
|
+
## Next steps
|
|
247
|
+
|
|
248
|
+
- [Reading Subscription Data](./subscription-data.md) — the full subscription
|
|
249
|
+
object you can read in code.
|
|
250
|
+
- [Dynamic Metering](./dynamic-metering.md) — the full `setMeters` / `addMeters`
|
|
251
|
+
/ `getMeters` API and how static and runtime values merge.
|
|
252
|
+
- [Monetization Policy Reference](./monetization-policy.md) — every
|
|
253
|
+
configuration option.
|
|
254
|
+
- [Meters](./meters.mdx) — defining the meters your policies increment.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Reading Subscription Data
|
|
3
|
+
sidebar_label: Subscription Data
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
When the `MonetizationInboundPolicy` authenticates a request, it looks up the
|
|
7
|
+
caller's subscription — their plan, entitlements, payment status, and billing
|
|
8
|
+
dates — and stores it on the request context. Read that data from your own code
|
|
9
|
+
with the static `MonetizationInboundPolicy.getSubscriptionData` method to make
|
|
10
|
+
decisions, personalize responses, or log which plan a request ran on.
|
|
11
|
+
|
|
12
|
+
## Where you can call it
|
|
13
|
+
|
|
14
|
+
`getSubscriptionData` returns data that the monetization policy puts on the
|
|
15
|
+
context, so it only returns a value **after** the `monetization-inbound` policy
|
|
16
|
+
has run. Call it from:
|
|
17
|
+
|
|
18
|
+
- A **custom code inbound policy** placed _after_ `monetization-inbound` in the
|
|
19
|
+
route's inbound pipeline.
|
|
20
|
+
- The **route handler**, which always runs after inbound policies.
|
|
21
|
+
|
|
22
|
+
On a route without the monetization policy — or in a policy that runs before it
|
|
23
|
+
— the method returns `undefined`. Always handle that case.
|
|
24
|
+
|
|
25
|
+
:::note
|
|
26
|
+
|
|
27
|
+
The `monetization-inbound` policy must come before any policy that reads the
|
|
28
|
+
subscription. If your policy runs first, the subscription data isn't on the
|
|
29
|
+
context yet. See
|
|
30
|
+
[pipeline ordering](./monetization-policy.md#pipeline-ordering).
|
|
31
|
+
|
|
32
|
+
:::
|
|
33
|
+
|
|
34
|
+
## Basic usage
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import {
|
|
38
|
+
MonetizationInboundPolicy,
|
|
39
|
+
HttpProblems,
|
|
40
|
+
ZuploContext,
|
|
41
|
+
ZuploRequest,
|
|
42
|
+
} from "@zuplo/runtime";
|
|
43
|
+
|
|
44
|
+
export default async function (request: ZuploRequest, context: ZuploContext) {
|
|
45
|
+
const subscription = MonetizationInboundPolicy.getSubscriptionData(context);
|
|
46
|
+
|
|
47
|
+
if (!subscription) {
|
|
48
|
+
return HttpProblems.forbidden(request, context, {
|
|
49
|
+
detail: "No active subscription",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
context.log.info(`Request on plan: ${subscription.plan.key}`);
|
|
54
|
+
return request;
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## The subscription object
|
|
59
|
+
|
|
60
|
+
`getSubscriptionData` returns a `MonetizationSubscription`. The fields you reach
|
|
61
|
+
for most are the plan and the entitlements map:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
interface MonetizationSubscription {
|
|
65
|
+
id: string;
|
|
66
|
+
customerId: string;
|
|
67
|
+
name: string;
|
|
68
|
+
status: string;
|
|
69
|
+
currency: string;
|
|
70
|
+
|
|
71
|
+
plan: {
|
|
72
|
+
id: string;
|
|
73
|
+
name: string;
|
|
74
|
+
key: string; // Stable identifier — switch on this in code
|
|
75
|
+
version: number;
|
|
76
|
+
description?: string;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Keyed by meter or feature key
|
|
80
|
+
entitlements: Record<
|
|
81
|
+
string,
|
|
82
|
+
{
|
|
83
|
+
balance: number; // Remaining allowance this period
|
|
84
|
+
hasAccess: boolean; // false when no access or quota spent
|
|
85
|
+
overage: number; // Usage beyond the included allowance
|
|
86
|
+
usage: number; // Consumed this period
|
|
87
|
+
}
|
|
88
|
+
>;
|
|
89
|
+
|
|
90
|
+
paymentStatus?: {
|
|
91
|
+
status: "paid" | "not_required" | "pending" | "failed" | "uncollectible";
|
|
92
|
+
isFirstPayment: boolean;
|
|
93
|
+
lastPaymentFailedAt?: string;
|
|
94
|
+
lastPaymentSucceededAt?: string;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
billingCadence: string; // ISO 8601 duration, e.g. "P1M" for monthly
|
|
98
|
+
billingAnchor: string;
|
|
99
|
+
nextBillingDate: string;
|
|
100
|
+
activeFrom: string;
|
|
101
|
+
activeTo?: string;
|
|
102
|
+
|
|
103
|
+
maxPaymentOverdueDays: number;
|
|
104
|
+
accessBlocked?: boolean;
|
|
105
|
+
createdAt: string;
|
|
106
|
+
updatedAt: string;
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Switch on `plan.key` rather than `plan.name` in your logic — the key is a stable
|
|
111
|
+
identifier, while the name is a display label that can change.
|
|
112
|
+
|
|
113
|
+
## Reading entitlements
|
|
114
|
+
|
|
115
|
+
Each entry in `entitlements` describes one metered feature or static feature on
|
|
116
|
+
the subscription. The key is the meter or feature key; the value reports the
|
|
117
|
+
caller's standing against it:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
const subscription = MonetizationInboundPolicy.getSubscriptionData(context);
|
|
121
|
+
|
|
122
|
+
const apiCalls = subscription?.entitlements["api_requests"];
|
|
123
|
+
if (apiCalls) {
|
|
124
|
+
context.log.info(
|
|
125
|
+
`api_requests — used ${apiCalls.usage}, ${apiCalls.balance} remaining`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
- `hasAccess` is the quickest check for "can this caller use this feature" —
|
|
131
|
+
it's `false` when the plan doesn't include the feature or the quota has run
|
|
132
|
+
out.
|
|
133
|
+
- `balance` is the remaining allowance. A balance of `0` or less means no
|
|
134
|
+
allowance remains.
|
|
135
|
+
- `usage` and `overage` report consumption this billing period.
|
|
136
|
+
|
|
137
|
+
## Caveats
|
|
138
|
+
|
|
139
|
+
- **Returns `undefined`** when the monetization policy hasn't run. Guard every
|
|
140
|
+
call.
|
|
141
|
+
- **The policy caches the data.** Subscription and entitlement data is cached
|
|
142
|
+
for up to `cacheTtlSeconds` (60 seconds minimum), so `balance`, `usage`, and
|
|
143
|
+
`overage` can lag real-time consumption by the length of the cache window.
|
|
144
|
+
Treat them as recent, not exact.
|
|
145
|
+
|
|
146
|
+
## Next steps
|
|
147
|
+
|
|
148
|
+
- [Programmatic Monetization](./programmatic-monetization.md) — gate operations
|
|
149
|
+
by plan and meter requests based on the response.
|
|
150
|
+
- [Dynamic Metering](./dynamic-metering.md) — set meter values at runtime from
|
|
151
|
+
code.
|
|
152
|
+
- [Monetization Policy Reference](./monetization-policy.md) — every policy
|
|
153
|
+
configuration option.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: OpenAPI Format Validation Warnings in Local Development
|
|
3
|
+
sidebar_label: Format Validation Warnings
|
|
4
|
+
description:
|
|
5
|
+
Understand the "unknown format ... ignored" warnings the Zuplo CLI prints
|
|
6
|
+
during local development, why they appear, and why they are safe to ignore.
|
|
7
|
+
tags:
|
|
8
|
+
- openapi
|
|
9
|
+
- local-development
|
|
10
|
+
- validation
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
When you run a Zuplo project locally with `npm run dev` (or `zuplo dev`), the
|
|
14
|
+
console may print one or more warnings like this:
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
unknown format "date-time" ignored in schema at path "#/properties/createdAt"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
These warnings are informational. They do not stop the development server, fail
|
|
21
|
+
schema validation, or change how your gateway behaves once deployed. This page
|
|
22
|
+
explains what the warning means and what, if anything, to do about it.
|
|
23
|
+
|
|
24
|
+
:::tip
|
|
25
|
+
|
|
26
|
+
**Short answer: the warnings are safe to ignore.** They report that a `format`
|
|
27
|
+
keyword in your OpenAPI document is not being actively checked — the field is
|
|
28
|
+
still validated against its `type` and every other constraint you defined.
|
|
29
|
+
|
|
30
|
+
:::
|
|
31
|
+
|
|
32
|
+
## What the warning means
|
|
33
|
+
|
|
34
|
+
Zuplo validates requests against your OpenAPI schema using
|
|
35
|
+
[Ajv](https://ajv.js.org/), a standard JSON Schema validator. In JSON Schema and
|
|
36
|
+
OpenAPI, the
|
|
37
|
+
[`format` keyword](https://json-schema.org/understanding-json-schema/reference/string#format)
|
|
38
|
+
(for example `date-time`, `email`, or `uuid`) is an _annotation_. A validator
|
|
39
|
+
only enforces a format if it has a validator registered for that specific
|
|
40
|
+
format. For any format it does not recognize, the standard behavior is to skip
|
|
41
|
+
the format check and log a message like the one above.
|
|
42
|
+
|
|
43
|
+
When you see `unknown format "date-time" ignored in schema at path "…"`, it
|
|
44
|
+
means:
|
|
45
|
+
|
|
46
|
+
- The validator found a `format: "date-time"` (or similar) in your schema.
|
|
47
|
+
- It has no registered check for that format in local development, so it
|
|
48
|
+
**ignores the format** and moves on.
|
|
49
|
+
- The field is **still validated** against its `type` and any other keywords you
|
|
50
|
+
set, such as `pattern`, `enum`, `minimum`, `maxLength`, or `required`.
|
|
51
|
+
|
|
52
|
+
The `path` in the message (for example `#/properties/createdAt`, or a path or
|
|
53
|
+
query parameter location) points to where the format appears in the schema, so
|
|
54
|
+
you can locate it.
|
|
55
|
+
|
|
56
|
+
## Are the warnings safe to ignore?
|
|
57
|
+
|
|
58
|
+
Yes. The warning is purely informational. It does **not**:
|
|
59
|
+
|
|
60
|
+
- Stop or crash the local development server.
|
|
61
|
+
- Cause a build or deployment to fail.
|
|
62
|
+
- Reject any request, or change the response your API returns.
|
|
63
|
+
- Change validation behavior in deployed environments.
|
|
64
|
+
|
|
65
|
+
Type checking and every other constraint in your schema continue to work exactly
|
|
66
|
+
as written. An enforced `format` validates the _shape_ of a value — for example,
|
|
67
|
+
that a string looks like an RFC 3339 date-time. When a format is ignored, you
|
|
68
|
+
lose only that one extra check; the field's `type` and all other constraints are
|
|
69
|
+
unaffected.
|
|
70
|
+
|
|
71
|
+
## Why some formats warn and others don't
|
|
72
|
+
|
|
73
|
+
You may notice the warning for some formats but not others, and on some fields
|
|
74
|
+
but not others. A few things drive this:
|
|
75
|
+
|
|
76
|
+
- **`format` is advisory in JSON Schema.** Validators are free to enforce only
|
|
77
|
+
the formats they choose to register. Which formats are actively checked is an
|
|
78
|
+
internal detail of the validator and can differ between CLI versions, so treat
|
|
79
|
+
the set as subject to change rather than a fixed contract.
|
|
80
|
+
- **Path and query parameters are validated separately from request bodies.**
|
|
81
|
+
Parameter schemas and body schemas are generally compiled independently, so
|
|
82
|
+
the same `format` can produce a warning in one place but not the other.
|
|
83
|
+
|
|
84
|
+
Because the warning is harmless, you do not need to determine exactly which
|
|
85
|
+
formats are checked. The guidance below applies to any format that warns.
|
|
86
|
+
|
|
87
|
+
## How to remove the warnings
|
|
88
|
+
|
|
89
|
+
You have two options, depending on whether you rely on that format for
|
|
90
|
+
validation.
|
|
91
|
+
|
|
92
|
+
**Option 1 — Leave the `format` keyword in place (recommended).** Keeping
|
|
93
|
+
`format` annotations is good practice: they document intent, drive code
|
|
94
|
+
generation and the developer portal, and are useful to consumers of your API
|
|
95
|
+
even when the gateway does not enforce them. The warning is the only cost, and
|
|
96
|
+
it is cosmetic.
|
|
97
|
+
|
|
98
|
+
**Option 2 — Enforce the value with a constraint that _is_ checked.** If you
|
|
99
|
+
want the gateway to actively reject malformed values, replace or supplement the
|
|
100
|
+
`format` with explicit constraints that the validator always enforces, such as
|
|
101
|
+
`pattern`, `enum`, `minLength`/`maxLength`, or `minimum`/`maximum`. For example,
|
|
102
|
+
to enforce a date-time-like string with a regular expression instead of relying
|
|
103
|
+
on `format`:
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"createdAt": {
|
|
108
|
+
"type": "string",
|
|
109
|
+
"format": "date-time",
|
|
110
|
+
"pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})$"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Here `format` stays for documentation, and `pattern` does the actual
|
|
116
|
+
enforcement. The regular expression above is an illustrative RFC 3339-style
|
|
117
|
+
example, not a Zuplo-mandated pattern — adjust it to match the values you
|
|
118
|
+
expect. The warning may still appear for the ignored `format`, but the value is
|
|
119
|
+
now strictly validated.
|
|
120
|
+
|
|
121
|
+
:::caution
|
|
122
|
+
|
|
123
|
+
The [Request Validation policy](../policies/request-validation-inbound.mdx)
|
|
124
|
+
options control _what_ is validated (`validateBody`, `validateQueryParameters`,
|
|
125
|
+
`validatePathParameters`, `validateHeaders`) and how failures are handled, but
|
|
126
|
+
they do not let you register additional formats or silence individual format
|
|
127
|
+
warnings. To guarantee a value's shape, use enforced constraints like `pattern`
|
|
128
|
+
as shown above.
|
|
129
|
+
|
|
130
|
+
:::
|
|
131
|
+
|
|
132
|
+
## Related
|
|
133
|
+
|
|
134
|
+
- [Request Validation Policy](../policies/request-validation-inbound.mdx) —
|
|
135
|
+
validate request bodies, query parameters, path parameters, and headers
|
|
136
|
+
against your OpenAPI schema.
|
|
137
|
+
- [Schema Validation Failed (SCHEMA_VALIDATION_FAILED)](../errors/schema-validation-failed.mdx)
|
|
138
|
+
— the runtime error returned when a request does not pass schema validation.
|
|
139
|
+
- [OpenAPI Support in Zuplo](./openapi.mdx) — how Zuplo uses your OpenAPI
|
|
140
|
+
document to configure the gateway.
|
|
141
|
+
- [Local Development](./local-development.mdx) — run and test your gateway
|
|
142
|
+
locally with the Zuplo CLI.
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Troubleshooting Stuck Deployments and Git Sync Errors
|
|
3
|
+
sidebar_label: Troubleshooting Deployments
|
|
4
|
+
description:
|
|
5
|
+
Diagnose Zuplo deployments that hang at "Deploying api" or "Building Developer
|
|
6
|
+
Portal" and fix Git source-control sync errors such as Bitbucket 410 Gone
|
|
7
|
+
responses on branch retrieval.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
When a deploy seems to hang at `Deploying api…` or `Building Developer Portal…`,
|
|
11
|
+
or your Git provider stops returning branches with a `Bad Request` or `410 Gone`
|
|
12
|
+
error, the cause is usually one of a few known issues. This guide helps you tell
|
|
13
|
+
a genuinely stuck deploy from a slow-but-progressing one, fix Git source-control
|
|
14
|
+
sync failures, and gather the right details before contacting support.
|
|
15
|
+
|
|
16
|
+
## How a Zuplo deploy progresses
|
|
17
|
+
|
|
18
|
+
Every deploy, whether triggered by a Git push, the Zuplo CLI, or a save in the
|
|
19
|
+
Portal, runs through two stages in order:
|
|
20
|
+
|
|
21
|
+
1. **`Deploying api…`**: Zuplo builds your gateway configuration (routes,
|
|
22
|
+
policies, and modules) and rolls it out to the edge.
|
|
23
|
+
2. **`Building Developer Portal…`**: Zuplo builds the Developer Portal site from
|
|
24
|
+
your OpenAPI document and portal configuration.
|
|
25
|
+
|
|
26
|
+
Each stage produces its own build logs. Zuplo separates logs by stage: `api` for
|
|
27
|
+
the gateway build and `dev-portal` for the Developer Portal build. Knowing which
|
|
28
|
+
stage a deploy stalled in tells you where to look.
|
|
29
|
+
|
|
30
|
+
:::note
|
|
31
|
+
|
|
32
|
+
A deploy can finish the `api` stage successfully and still be working on the
|
|
33
|
+
`dev-portal` stage. If your gateway is already serving traffic but the deploy
|
|
34
|
+
status hasn't flipped to complete, the Developer Portal build is the most likely
|
|
35
|
+
place it's still working. See
|
|
36
|
+
[The Developer Portal build never finishes](#the-developer-portal-build-never-finishes).
|
|
37
|
+
|
|
38
|
+
:::
|
|
39
|
+
|
|
40
|
+
## Is the deploy stuck, or still working?
|
|
41
|
+
|
|
42
|
+
A deploy that looks frozen in the terminal is often still running on Zuplo's
|
|
43
|
+
side. The Zuplo CLI doesn't run the build itself. It starts the deploy and then
|
|
44
|
+
_polls_ for the result. The build continues on the server even if the CLI stops
|
|
45
|
+
waiting.
|
|
46
|
+
|
|
47
|
+
### Confirm the server-side status first
|
|
48
|
+
|
|
49
|
+
Before assuming a deploy failed, check whether it actually completed:
|
|
50
|
+
|
|
51
|
+
1. Open your [project](https://portal.zuplo.com/+/account/project/) in the Zuplo
|
|
52
|
+
Portal.
|
|
53
|
+
2. Check the environment you deployed to. If the build finished, the environment
|
|
54
|
+
shows the new deployment and its URL responds to requests.
|
|
55
|
+
3. Send a request to the environment URL (or its
|
|
56
|
+
[health check route](./health-checks.mdx), if you have one) to confirm the
|
|
57
|
+
gateway is live.
|
|
58
|
+
|
|
59
|
+
If the environment is updated and serving traffic, the deploy succeeded. The CLI
|
|
60
|
+
simply stopped waiting before the build reported back.
|
|
61
|
+
|
|
62
|
+
:::tip
|
|
63
|
+
|
|
64
|
+
The CLI prints `Deployed to https://...` on success. If you never saw that line
|
|
65
|
+
but the Portal shows the environment updated, the deploy completed after the CLI
|
|
66
|
+
timed out. Capture the URL from the Portal rather than constructing it from the
|
|
67
|
+
branch name. The hostname uses a normalized, truncated form of the environment
|
|
68
|
+
name plus a unique identifier.
|
|
69
|
+
|
|
70
|
+
:::
|
|
71
|
+
|
|
72
|
+
### Extend the CLI polling timeout
|
|
73
|
+
|
|
74
|
+
By default the CLI polls every second for up to 250 attempts, a little over four
|
|
75
|
+
minutes. Large projects can take longer to build, and when the CLI's poll budget
|
|
76
|
+
runs out it stops waiting even though the deploy keeps running on the server.
|
|
77
|
+
|
|
78
|
+
Increase the timeout with the `POLL_INTERVAL` and `MAX_POLL_RETRIES` environment
|
|
79
|
+
variables:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Poll every 5 seconds for up to 300 attempts (25 minutes)
|
|
83
|
+
POLL_INTERVAL=5000 MAX_POLL_RETRIES=300 zuplo deploy
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- **`POLL_INTERVAL`**: Milliseconds between polls. Default `1000` (1 second).
|
|
87
|
+
- **`MAX_POLL_RETRIES`**: Maximum number of polls before the CLI times out.
|
|
88
|
+
Default `250`.
|
|
89
|
+
|
|
90
|
+
For the full command reference, see [CLI: deploy](../cli/deploy.mdx).
|
|
91
|
+
|
|
92
|
+
:::caution
|
|
93
|
+
|
|
94
|
+
Raising the polling timeout only changes how long the CLI _waits_. It does not
|
|
95
|
+
make the build faster, and it does not fix a build that is genuinely failing. If
|
|
96
|
+
the deploy never completes server-side no matter how long you wait, treat it as
|
|
97
|
+
a failed build and read the logs for the stage that stalled.
|
|
98
|
+
|
|
99
|
+
:::
|
|
100
|
+
|
|
101
|
+
## The Developer Portal build never finishes
|
|
102
|
+
|
|
103
|
+
If the gateway (`api` stage) deploys but the overall deploy hangs at
|
|
104
|
+
`Building Developer Portal…`, the problem is in the Developer Portal build, not
|
|
105
|
+
your routes or policies.
|
|
106
|
+
|
|
107
|
+
Common causes:
|
|
108
|
+
|
|
109
|
+
- **Invalid OpenAPI document**: The portal is generated from your OpenAPI
|
|
110
|
+
document. A malformed `routes.oas.json`, an unresolved `$ref`, or invalid
|
|
111
|
+
schema can stall or fail the portal build. Validate your OpenAPI document and
|
|
112
|
+
fix any errors.
|
|
113
|
+
- **A legacy `config/dev-portal.json` file**: Projects migrated from the old
|
|
114
|
+
Developer Portal can carry a stale `config/dev-portal.json` that breaks the
|
|
115
|
+
build. See the [Dev Portal Migration Guide](../dev-portal/migration.mdx) for
|
|
116
|
+
the exact cleanup steps.
|
|
117
|
+
- **Custom portal configuration errors**: Errors in your `zudoku.config.tsx` (or
|
|
118
|
+
other portal configuration) can prevent the site from building.
|
|
119
|
+
|
|
120
|
+
Read the `dev-portal` stage build logs to see the specific error, then redeploy
|
|
121
|
+
after fixing it.
|
|
122
|
+
|
|
123
|
+
## Git source-control sync errors
|
|
124
|
+
|
|
125
|
+
Zuplo connects to GitHub, GitLab, Bitbucket, and Azure DevOps for source
|
|
126
|
+
control. The integration pushes and pulls code between the Portal and your
|
|
127
|
+
repository, and it lists branches so you can map them to environments. When that
|
|
128
|
+
authorization breaks, branch retrieval fails, often with a `Bad Request` or
|
|
129
|
+
`410 Gone` error.
|
|
130
|
+
|
|
131
|
+
### Why branch retrieval returns `Bad Request` or `410 Gone`
|
|
132
|
+
|
|
133
|
+
These errors come from the Git provider, not from Zuplo, and they almost always
|
|
134
|
+
mean the connection is no longer authorized:
|
|
135
|
+
|
|
136
|
+
- The OAuth authorization Zuplo uses to reach the provider has **expired or been
|
|
137
|
+
revoked**.
|
|
138
|
+
- The repository was **renamed or moved**, which breaks the existing connection.
|
|
139
|
+
- The Git app was **uninstalled** or lost access to the repository in the
|
|
140
|
+
provider's settings.
|
|
141
|
+
|
|
142
|
+
`410 Gone` in particular signals that the resource Zuplo asked for (the branch
|
|
143
|
+
list) is no longer available at that location, typically because the
|
|
144
|
+
authorization or repository link behind it is stale.
|
|
145
|
+
|
|
146
|
+
### Reconnect the integration
|
|
147
|
+
|
|
148
|
+
To restore branch sync, re-authorize the connection:
|
|
149
|
+
|
|
150
|
+
1. Open your
|
|
151
|
+
[project settings](https://portal.zuplo.com/+/account/project/settings/general)
|
|
152
|
+
in the Zuplo Portal and select **Source Control**.
|
|
153
|
+
2. Disconnect the current repository connection.
|
|
154
|
+
3. Reconnect and complete the provider's authorization flow again, granting
|
|
155
|
+
access to the repository that holds your Zuplo project.
|
|
156
|
+
|
|
157
|
+
After reconnecting, retrieving branches should succeed again.
|
|
158
|
+
|
|
159
|
+
:::caution
|
|
160
|
+
|
|
161
|
+
Renaming a repository breaks the Zuplo connection. If you renamed or moved the
|
|
162
|
+
repository, disconnect and reconnect to restore the link. See
|
|
163
|
+
[Rename or Move Project](./rename-or-move-project.mdx) for details.
|
|
164
|
+
|
|
165
|
+
:::
|
|
166
|
+
|
|
167
|
+
### Bitbucket-specific notes
|
|
168
|
+
|
|
169
|
+
Bitbucket integration is available on
|
|
170
|
+
[enterprise plans](https://zuplo.com/pricing) and provides push/pull source
|
|
171
|
+
control without automatic deployments. You deploy with the Zuplo CLI through
|
|
172
|
+
[Bitbucket Pipelines](./custom-ci-cd-bitbucket.mdx).
|
|
173
|
+
|
|
174
|
+
If reconnecting from the Portal doesn't clear the sync error:
|
|
175
|
+
|
|
176
|
+
- **Confirm Bitbucket is still enabled for your account.** For
|
|
177
|
+
[bitbucket.org](https://bitbucket.org), Zuplo support enables the integration.
|
|
178
|
+
Contact [support@zuplo.com](mailto:support@zuplo.com) with your Bitbucket
|
|
179
|
+
Workspace ID (found on your Workspace Settings page).
|
|
180
|
+
- **For self-hosted Bitbucket, check the OAuth app.** If the OAuth app's client
|
|
181
|
+
secret was rotated or its callback URL or permissions changed, branch
|
|
182
|
+
retrieval fails until the app is reconfigured. The callback URL must be
|
|
183
|
+
`https://portal.zuplo.com` and the app must grant the `repo`, `user`, and
|
|
184
|
+
`read:org` permissions. See
|
|
185
|
+
[Bitbucket Setup](./source-control-setup-bitbucket.mdx).
|
|
186
|
+
|
|
187
|
+
## Decision tree
|
|
188
|
+
|
|
189
|
+
Use this to route yourself to the right fix:
|
|
190
|
+
|
|
191
|
+
| Symptom | Most likely cause | First action |
|
|
192
|
+
| ---------------------------------------------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
193
|
+
| CLI hangs at `Deploying api…` then times out | Build took longer than the CLI's poll budget | Check the environment in the Portal; raise `MAX_POLL_RETRIES` |
|
|
194
|
+
| CLI reported timeout, but the environment is updated | Deploy completed after the CLI stopped waiting | None; capture the URL from the Portal |
|
|
195
|
+
| Deploy hangs at `Building Developer Portal…` | Developer Portal build error | Read the `dev-portal` build logs; validate OpenAPI; check for a stale `config/dev-portal.json` |
|
|
196
|
+
| Branch list fails with `Bad Request` or `410 Gone` | Git authorization expired, revoked, or stale | Reconnect the integration in **Source Control** settings |
|
|
197
|
+
| Bitbucket still fails after reconnecting | Account not enabled, or OAuth app misconfigured | Contact support with your Workspace ID; check the self-hosted OAuth app |
|
|
198
|
+
|
|
199
|
+
## When to contact support
|
|
200
|
+
|
|
201
|
+
If you've worked through the relevant section above and the deploy or sync still
|
|
202
|
+
fails, contact [support@zuplo.com](mailto:support@zuplo.com). Include these
|
|
203
|
+
details so support can investigate without a round-trip:
|
|
204
|
+
|
|
205
|
+
- Your **account** and **project** names.
|
|
206
|
+
- The **environment** (branch) you're deploying to.
|
|
207
|
+
- The **stage** where it stalls (`api` or `dev-portal`) and the exact message
|
|
208
|
+
you see.
|
|
209
|
+
- For sync errors: your **Git provider**, the **repository**, and the exact
|
|
210
|
+
error text (for example, `410 Gone` on branch retrieval).
|
|
211
|
+
- The **approximate time** of the failed deploy, in UTC.
|
|
212
|
+
|
|
213
|
+
## Next steps
|
|
214
|
+
|
|
215
|
+
- [Source Control & Deployments](./source-control.mdx): how source control and
|
|
216
|
+
deployments fit together across providers.
|
|
217
|
+
- [Branch-Based Deployments](./branch-based-deployments.mdx): how branches map
|
|
218
|
+
to environments and how environment URLs are named.
|
|
219
|
+
- [Deploying Zuplo from a Monorepo](./monorepo-deployment.mdx): CI/CD
|
|
220
|
+
configuration, schema validation, and health-check timeouts.
|
|
221
|
+
- [Troubleshooting (local development)](./local-development-troubleshooting.mdx):
|
|
222
|
+
local dev server, ports, and certificate issues.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zuplo",
|
|
3
|
-
"version": "6.71.
|
|
3
|
+
"version": "6.71.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The programmable API Gateway",
|
|
6
6
|
"author": "Zuplo, Inc.",
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
"zuplo": "zuplo.js"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@zuplo/cli": "6.71.
|
|
23
|
-
"@zuplo/core": "6.71.
|
|
24
|
-
"@zuplo/runtime": "6.71.
|
|
22
|
+
"@zuplo/cli": "6.71.9",
|
|
23
|
+
"@zuplo/core": "6.71.9",
|
|
24
|
+
"@zuplo/runtime": "6.71.9",
|
|
25
25
|
"@zuplo/test": "1.4.0"
|
|
26
26
|
}
|
|
27
27
|
}
|