zuplo 7.7.9 → 7.7.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -355,34 +355,23 @@ have both actions, the `warn` value must be less than the `block` value. The
355
355
  policy does not treat entries whose `budgetBy` is `app` as expression budget
356
356
  rules.
357
357
 
358
- ### Supported expressions
359
-
360
- Expressions read one string or safe integer from request-time data. Use these
361
- canonical forms when writing application configuration:
362
-
363
- | Source | Canonical expression example |
364
- | --------------------------- | ------------------------------------------- |
365
- | Request URL or method | `request.url`, `request.method` |
366
- | One request header | `request.headers.get("x-customer-id")` |
367
- | Authenticated subject | `request.user.sub` |
368
- | User data | `request.user.data.profile.organizationId` |
369
- | Query parameter | `request.query.customerId` |
370
- | Query parameter (map form) | `request.searchParams.customerId` |
371
- | Route parameter | `request.params.productId` |
372
- | Custom context data | `context.custom.account.id` |
373
- | Route data | `context.route.path` |
374
- | Incoming request data | `context.incomingRequestProperties.country` |
375
- | Request or context identity | `request.user.sub`, `context.requestId` |
376
- | A key requiring brackets | `request.user.data["team-id"]` |
377
-
378
- The `expression` field contains the complete expression. Do not wrap it in an
379
- interpolation marker such as `${...}`. Expressions use values from the request
380
- and context, including values set by earlier policies. Place policies that set
381
- these values before Semantic Cache. You can change an expression in the
358
+ ### Expressions
359
+
360
+ An expression selects one value from the request or its context, such as
361
+ `request.headers.get("x-user-id")` or `request.user.data["team-id"]`. Every
362
+ distinct value gets its own budget.
363
+
364
+ The expression grammar, the selectable properties, what a value must be, and
365
+ what happens when an expression does not resolve are documented once, on the
366
+ [AI Gateway Metering](/docs/policies/ai-gateway-metering-v2-inbound) policy
367
+ page. The same grammar applies here.
368
+
369
+ Two things are specific to application configuration. A policy that sets a value
370
+ an expression reads must run before Semantic Cache, which can answer without
371
+ reaching the rest of the chain. And you can change an expression in the
382
372
  application configuration without rebuilding or redeploying the gateway.
383
373
 
384
- JSON encoding and expression syntax are separate. When writing raw JSON, escape
385
- the double quotes required by a bracket segment:
374
+ When writing raw JSON, escape the double quotes a bracket segment needs:
386
375
 
387
376
  ```json
388
377
  {
@@ -390,58 +379,14 @@ the double quotes required by a bracket segment:
390
379
  }
391
380
  ```
392
381
 
393
- After a JSON parser decodes this value, the expression is
382
+ After a JSON parser decodes that value the expression is
394
383
  `request.user.data["team-id"]`; the backslashes are not part of its identity.
395
- Code that creates application configuration should build an ordinary string and
396
- let its JSON serializer handle the transport escaping:
384
+ Code that builds application configuration should create an ordinary string and
385
+ let its JSON serializer add the transport escaping.
397
386
 
398
- ```ts
399
- const expression = 'request.user.data["team-id"]';
400
- const body = JSON.stringify({ expression });
401
- ```
402
-
403
- Header expressions are terminal. Header names are case-insensitive; use
404
- lowercase in stored expressions. The parser accepts the canonical
405
- `request.headers.get("content-type")` form and the equivalent
406
- `request.headers.content-type` and `request.headers["content-type"]` forms. The
407
- canonical form keeps header access visually distinct from an ordinary object
408
- property.
409
-
410
- A dot property starts with an ASCII letter, `_`, or `$`, followed by those
411
- characters, ASCII digits, or hyphens. Canonical rendering uses quoted brackets
412
- for keys containing `$` or `-`, or any other non-identifier character. For
413
- example, `context.custom.team-id` is accepted and its canonical form is
414
- `context.custom["team-id"]`. Single- and double-quoted bracket properties are
415
- accepted. Whitespace immediately inside brackets is accepted but is not
416
- canonical.
417
-
418
- The selectable data model is:
419
-
420
- | Root | Selectable properties |
421
- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
422
- | `request` | `url`, `method`, `headers`, `user.sub`, `user.data`, `query`, `searchParams`, `params`, and the scalar Fetch request metadata `bodyUsed`, `cache`, `credentials`, `destination`, `integrity`, `keepalive`, `mode`, `redirect`, `referrer`, and `referrerPolicy` |
423
- | `context` | `contextId`, `requestId`, `custom`, `route`, and `incomingRequestProperties` |
424
-
425
- Expressions cannot read the request body, call logging or policy methods, or
426
- access properties outside this table.
427
-
428
- Expressions have these limits:
429
-
430
- - The expression is at most 1,024 UTF-8 bytes and contains well-formed Unicode.
431
- - Quoted keys are non-empty and do not contain double quotes, backslashes,
432
- control characters, U+2028, or U+2029. String escapes are not supported.
433
- - Canonical expressions contain no syntax whitespace. Spaces inside a quoted key
434
- are part of the key.
435
- - Wildcards, recursive descent, filters, numeric bracket indexes, calls other
436
- than the terminal header `.get("name")`, function arguments, and roots other
437
- than `request` and `context` are not supported. For example,
438
- `request.url.substring(5, 1)` is not a supported expression.
439
- - Traversal reads own properties from plain objects and keys from declared Maps.
440
- It does not traverse arrays, class instances, inherited properties, or the
441
- property names `__proto__`, `constructor`, and `prototype`.
442
-
443
- An authentication policy can populate `request.user`. A custom policy earlier in
444
- the chain can derive a value and place it in a request header:
387
+ An expression selects one value and cannot combine several. To budget by a
388
+ composite value, compute it in a custom policy earlier in the chain and store it
389
+ somewhere an expression can read:
445
390
 
446
391
  ```ts
447
392
  import { ZuploContext, ZuploRequest } from "@zuplo/runtime";
@@ -463,19 +408,6 @@ The corresponding expression is `request.headers.get("x-budget-customer")`.
463
408
  Configuration code should create that ordinary string and pass the enclosing
464
409
  object to `JSON.stringify`; the serializer adds the JSON transport escaping.
465
410
 
466
- The expression chooses one value; it does not concatenate values, run
467
- JavaScript, or evaluate conditions. Compute composite values in an earlier
468
- policy and store the result in a supported request or context property.
469
-
470
- The selected value must be a string or a safe integer. The runtime converts the
471
- value to well-formed NFC Unicode and ignores it when it exceeds 256 UTF-8 bytes
472
- or contains control characters, U+2028, or U+2029. A missing or invalid value
473
- does not contribute to the rule for that request.
474
-
475
- Use consistent spelling for each expression. Different spellings, such as single
476
- quotes and double quotes, identify different budget rules even when they select
477
- the same value.
478
-
479
411
  Invalid budget rules and unsupported expressions are logged and skipped. Other
480
412
  valid rules continue to apply.
481
413
 
@@ -68,6 +68,162 @@ value of its expression. An action of `"warn"` notifies without blocking. An
68
68
  action of `"block"` activates the configured quota fallback or returns
69
69
  `429 Too Many Requests` when usage reaches the value.
70
70
 
71
+ ## Budget expressions
72
+
73
+ A rule with `"budgetBy": "expression"` gives every distinct value of its
74
+ expression its own budget. A daily cost budget of five dollars on
75
+ `request.headers.get("x-user-id")` gives each user five dollars a day, rather
76
+ than five dollars shared across all users.
77
+
78
+ An expression selects one value from the request or its context. It is a
79
+ selector, not code: there are no comparisons, arithmetic, string concatenation,
80
+ or method calls other than the header accessor below.
81
+
82
+ ### Supported expressions
83
+
84
+ | Expression | Example | Selects |
85
+ | ------------------------------------------ | ------------------------------------------- | --------------------------------------------------------------- |
86
+ | `request.headers.get("<name>")` | `request.headers.get("x-user-id")` | One request header. Header names are case-insensitive. |
87
+ | `request.query.<name>` | `request.query.tenant` | One query-string parameter. |
88
+ | `request.searchParams.<name>` | `request.searchParams.tenant` | One query-string parameter. |
89
+ | `request.params.<name>` | `request.params.customerId` | One path parameter from the matched route. |
90
+ | `request.user.sub` | `request.user.sub` | The authenticated consumer's subject, such as the API key name. |
91
+ | `request.user.data.<property>` | `request.user.data.organizationId` | A property of the consumer's metadata, including nested ones. |
92
+ | `request.<property>` | `request.method` | A scalar request property. |
93
+ | `context.custom.<property>` | `context.custom.tenantId` | A value your own policies or handlers put on `context.custom`. |
94
+ | `context.route.<property>` | `context.route.path` | A property of the matched route, such as `path` or `label`. |
95
+ | `context.incomingRequestProperties.<name>` | `context.incomingRequestProperties.country` | A connection property, such as `country`, `city`, or `asn`. |
96
+
97
+ The selectable scalar request properties are `bodyUsed`, `cache`, `credentials`,
98
+ `destination`, `integrity`, `keepalive`, `method`, `mode`, `redirect`,
99
+ `referrer`, `referrerPolicy`, and `url`.
100
+
101
+ Use quoted brackets for any key that is not a plain identifier:
102
+ `request.query["team-id"]`, `context.custom["tenant.id"]`.
103
+
104
+ `context.incomingRequestProperties` exposes connection data only: `country`,
105
+ `city`, `region`, `regionCode`, `continent`, `colo`, `latitude`, `longitude`,
106
+ `postalCode`, `metroCode`, `timezone`, `ip`, `asn`, `asOrganization`, and
107
+ `httpProtocol`. Client-certificate and mTLS verification fields are not
108
+ selectable. To budget per client certificate, run the mTLS Authentication policy
109
+ first and select the metadata it attaches, such as
110
+ `request.user.data.mtlsAuth.sha256Fingerprint`.
111
+
112
+ An expression is limited to 1024 bytes and 32 property segments.
113
+
114
+ ### Examples
115
+
116
+ Give every end user their own daily spend budget, keyed by a header your
117
+ application sends:
118
+
119
+ ```json
120
+ {
121
+ "budgetBy": "expression",
122
+ "expression": "request.headers.get(\"x-user-id\")",
123
+ "meters": [
124
+ { "meter": "cost", "period": "daily", "value": 5, "action": "block" }
125
+ ]
126
+ }
127
+ ```
128
+
129
+ Give every customer of yours their own monthly token budget, keyed by metadata
130
+ on the API key that made the request:
131
+
132
+ ```json
133
+ {
134
+ "budgetBy": "expression",
135
+ "expression": "request.user.data.organizationId",
136
+ "meters": [
137
+ {
138
+ "meter": "tokens",
139
+ "period": "monthly",
140
+ "value": 8000000,
141
+ "action": "warn"
142
+ },
143
+ {
144
+ "meter": "tokens",
145
+ "period": "monthly",
146
+ "value": 10000000,
147
+ "action": "block"
148
+ }
149
+ ]
150
+ }
151
+ ```
152
+
153
+ Cap requests per tenant per hour, keyed by a path parameter on a route such as
154
+ `/tenants/:tenantId/chat`:
155
+
156
+ ```json
157
+ {
158
+ "budgetBy": "expression",
159
+ "expression": "request.params.tenantId",
160
+ "meters": [
161
+ {
162
+ "meter": "requests",
163
+ "period": "hourly",
164
+ "value": 1000,
165
+ "action": "block"
166
+ }
167
+ ]
168
+ }
169
+ ```
170
+
171
+ ### Values
172
+
173
+ An expression must resolve to a string or a safe integer. The runtime converts
174
+ the value to well-formed NFC Unicode, and ignores it when it exceeds 256 UTF-8
175
+ bytes or contains control characters, U+2028, or U+2029. Values are
176
+ case-sensitive, so `Acme` and `acme` get separate budgets, but two strings that
177
+ normalize to the same NFC text share one budget.
178
+
179
+ When an expression resolves to nothing, or to something else such as an object,
180
+ an array, or a fractional number, that rule does not count or block the request.
181
+ Other rules still apply. Pair a rule on a value you must enforce with a policy
182
+ that rejects requests missing it, such as API key authentication or request
183
+ validation.
184
+
185
+ ### Not supported
186
+
187
+ | Not supported | Use instead |
188
+ | ---------------------------------------------------- | ------------------------------------------------------------------- |
189
+ | Request body values, such as `request.body.userId` | A header, a query parameter, a path parameter, or API key metadata. |
190
+ | Whole objects, such as `request.headers` | One value, such as `request.headers.get("x-user-id")`. |
191
+ | Comparisons, `&&`, ternaries, and literals | One selector per rule. An application can have up to five rules. |
192
+ | String methods, such as `.toLowerCase()` | The value as the client sends it. |
193
+ | Wildcards and multiple keys in one expression | One rule per value you want to budget. |
194
+ | Arrays, class instances, and inherited properties | A plain value the gateway or your own policy sets. |
195
+ | Per-request identifiers, such as `context.requestId` | A value shared by many requests, such as a user or tenant. |
196
+
197
+ An unsupported expression does not fail the request. The rule is skipped, and
198
+ the request is not budgeted by it.
199
+
200
+ ### Keep the spelling stable
201
+
202
+ Each expression is stored exactly as you write it, and its budgets are tracked
203
+ under that exact text. `request.headers.get("x-user-id")` and
204
+ `request.headers["x-user-id"]` read the same header but keep separate budgets,
205
+ and editing a rule's expression starts its budgets over. Single quotes and
206
+ double quotes likewise identify different rules. Use the spellings in the table
207
+ above.
208
+
209
+ Several forms parse, so prefer the canonical one. A header is written
210
+ `request.headers.get("content-type")`, with a lowercase name; the equivalent
211
+ `request.headers.content-type` and `request.headers["content-type"]` are
212
+ accepted but keep separate budgets. A dot property starts with a letter, `_`, or
213
+ `$`; a key containing anything else takes quoted brackets, so
214
+ `context.custom.team-id` is canonically `context.custom["team-id"]`.
215
+
216
+ In raw JSON, escape the double quotes a bracket segment needs:
217
+
218
+ ```json
219
+ {
220
+ "expression": "request.user.data[\"team-id\"]"
221
+ }
222
+ ```
223
+
224
+ After a JSON parser decodes that value the expression is
225
+ `request.user.data["team-id"]`; the backslashes are not part of its identity.
226
+
71
227
  ## Team limits
72
228
 
73
229
  Application budgets apply only to that application. Parent team and gateway
@@ -3,6 +3,10 @@ requests and enforces spend, token, and request budgets. When a limit is
3
3
  exceeded, it uses the model selection's quota fallback when one is configured or
4
4
  returns `429 Too Many Requests`.
5
5
 
6
+ A budget rule covers the whole application, or gives every distinct value of an
7
+ expression its own budget, such as one budget per user with
8
+ `request.headers.get("x-user-id")`.
9
+
6
10
  Place it after Model Filtering and Fallback Model, and before policies such as
7
11
  Semantic Cache that may answer without calling a provider. Metering fails open
8
12
  by default when its service is unavailable; set `throwOnFailure` to `true` to
@@ -77,17 +77,19 @@
77
77
  "properties": {
78
78
  "budgetBy": {
79
79
  "type": "string",
80
- "enum": ["app", "expression"]
80
+ "enum": ["app", "expression"],
81
+ "description": "Budget the application as a whole (\"app\"), or give every distinct value of an expression its own budget (\"expression\")."
81
82
  },
82
83
  "expression": {
83
84
  "type": "string",
84
85
  "minLength": 1,
85
- "description": "Required when budgetBy is \"expression\"; forbidden for \"app\". Each distinct value gets its own budget."
86
+ "description": "Required when budgetBy is \"expression\"; forbidden for \"app\". Each distinct value gets its own budget. Select one value from the request or its context: request.headers.get(\"x-user-id\"), request.query.tenant, request.params.customerId, request.user.sub, request.user.data.organizationId, request.method, request.url, context.custom.tenantId, context.route.path, or context.incomingRequestProperties.country. Comparisons, arithmetic, string methods, and request body values are not supported."
86
87
  },
87
88
  "meters": {
88
89
  "type": "array",
89
90
  "minItems": 1,
90
91
  "maxItems": 24,
92
+ "description": "Thresholds this rule enforces. Add one entry per meter, period, and action you want.",
91
93
  "items": {
92
94
  "type": "object",
93
95
  "additionalProperties": false,
@@ -95,19 +97,23 @@
95
97
  "properties": {
96
98
  "meter": {
97
99
  "type": "string",
98
- "enum": ["cost", "requests", "tokens"]
100
+ "enum": ["cost", "requests", "tokens"],
101
+ "description": "What to measure: US dollars spent (\"cost\"), requests made (\"requests\"), or tokens used (\"tokens\")."
99
102
  },
100
103
  "period": {
101
104
  "type": "string",
102
- "enum": ["hourly", "daily", "weekly", "monthly"]
105
+ "enum": ["hourly", "daily", "weekly", "monthly"],
106
+ "description": "How often usage resets."
103
107
  },
104
108
  "value": {
105
109
  "type": "number",
106
- "exclusiveMinimum": 0
110
+ "exclusiveMinimum": 0,
111
+ "description": "The threshold for this meter and period. Cost is in US dollars."
107
112
  },
108
113
  "action": {
109
114
  "type": "string",
110
- "enum": ["warn", "block"]
115
+ "enum": ["warn", "block"],
116
+ "description": "What happens at the threshold: \"warn\" notifies and lets the request through, \"block\" uses the configured quota fallback model or returns 429 Too Many Requests."
111
117
  }
112
118
  }
113
119
  }
@@ -168,4 +168,38 @@ Certificates can't be issued until these records resolve. cert-manager checks
168
168
  the challenge URL from inside the cluster before it contacts the certificate
169
169
  authority.
170
170
 
171
- Next, [verify your installation](./verify.md).
171
+ ## Verify your installation
172
+
173
+ Use the [Zuplo self-hosted doctor](https://github.com/zuplo/self-hosted-doctor)
174
+ to check the installation. Doctor is a CLI that reads your installation through
175
+ your kubeconfig and probes its DNS, TLS, and HTTP endpoints. It checks the Helm
176
+ release, the `Configuration` resource, Deployment health, ingress reachability,
177
+ certificate issuance, DNS records, the management API, and the builder
178
+ configuration. It reads the installation and doesn't change it.
179
+
180
+ Download the archive for your machine from the
181
+ [releases page](https://github.com/zuplo/self-hosted-doctor/releases), verify
182
+ its checksum, extract it, and run the full suite against your current kubeconfig
183
+ context:
184
+
185
+ ```bash
186
+ ./zuplo-self-hosted-doctor verify
187
+ ```
188
+
189
+ Doctor exits `0` when no check failed, `1` when a check failed, and `3` when it
190
+ couldn't run. Each warning and failure names a diagnostic step and links to the
191
+ matching entry in [Troubleshooting](./troubleshooting.md).
192
+
193
+ To also check authenticated access to the management API, set `ZUPLO_API_KEY` in
194
+ your shell before running `verify`. Without a key, Doctor checks only that
195
+ unauthenticated requests are rejected.
196
+
197
+ The repository's README covers selecting individual checks, comparing a local
198
+ values file, private certificate authorities, and JSON output.
199
+
200
+ :::note
201
+
202
+ Doctor doesn't deploy a project. To confirm the full build-and-serve path, run
203
+ `npx zuplo deploy` against your account after Doctor passes.
204
+
205
+ :::
@@ -184,10 +184,10 @@ images, and the credentials to pull them.
184
184
  ## Get started
185
185
 
186
186
  1. Review the [requirements](./requirements.md) and run the preflight checks.
187
- 2. [Install](./install.md) the Helm chart.
188
- 3. [Verify the installation](./verify.md), including a complete deployment.
189
- 4. [Upgrade](./upgrade.md) to later chart versions when needed.
190
- 5. Use [troubleshooting](./troubleshooting.md) to resolve installation and
187
+ 2. [Install](./install.md) the Helm chart, then verify the installation with the
188
+ [Zuplo self-hosted doctor](https://github.com/zuplo/self-hosted-doctor) CLI.
189
+ 3. [Upgrade](./upgrade.md) to later chart versions when needed.
190
+ 4. Use [troubleshooting](./troubleshooting.md) to resolve installation and
191
191
  deployment errors.
192
192
 
193
193
  To discuss your specific requirements,
@@ -202,8 +202,9 @@ to HTTPS.
202
202
 
203
203
  This redirect doesn't affect certificate issuance. cert-manager creates a
204
204
  separate challenge Ingress that continues to serve on port 80. To test the
205
- ingress, use a hostname with no Ingress, as described in
206
- [Verify your install](./verify.md).
205
+ ingress, use a hostname with no Ingress. The
206
+ [Zuplo self-hosted doctor](https://github.com/zuplo/self-hosted-doctor) CLI does
207
+ this for you in its `ingress-reachable` check.
207
208
 
208
209
  ## Failed to deploy the environment but the cluster shows the build succeeding
209
210
 
@@ -41,13 +41,13 @@ installed with `helm plugin install https://github.com/databus23/helm-diff`.
41
41
 
42
42
  ## Verify the upgrade
43
43
 
44
- Work through [Verify your install](./verify.md) again. The gateway deployments
45
- already in the cluster keep serving through the upgrade; they are rebuilt only
46
- when you deploy them.
44
+ Run the [Zuplo self-hosted doctor](https://github.com/zuplo/self-hosted-doctor)
45
+ CLI again and confirm the release history records the new revision. The gateway
46
+ deployments already in the cluster keep serving through the upgrade; they are
47
+ rebuilt only when you deploy them.
47
48
 
48
49
  ```bash
49
- kubectl wait --for=condition=Available deployment --all \
50
- -n zuplo-system --timeout=5m
50
+ ./zuplo-self-hosted-doctor verify
51
51
  helm history zuplo -n zuplo
52
52
  ```
53
53
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zuplo",
3
- "version": "7.7.9",
3
+ "version": "7.7.11",
4
4
  "type": "module",
5
5
  "description": "The official Zuplo CLI for local development and platform management",
6
6
  "homepage": "https://zuplo.com/docs/cli/overview",
@@ -32,9 +32,9 @@
32
32
  "zuplo": "zuplo.js"
33
33
  },
34
34
  "dependencies": {
35
- "@zuplo/cli": "7.7.9",
36
- "@zuplo/core": "7.7.9",
37
- "@zuplo/runtime": "7.7.9",
38
- "@zuplo/test": "7.7.9"
35
+ "@zuplo/cli": "7.7.11",
36
+ "@zuplo/core": "7.7.11",
37
+ "@zuplo/runtime": "7.7.11",
38
+ "@zuplo/test": "7.7.11"
39
39
  }
40
40
  }