@daloyjs/core 0.35.2 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +22 -2
  2. package/bin/daloy.mjs +2 -0
  3. package/dist/adapters/bun.js +16 -9
  4. package/dist/adapters/deno.js +7 -1
  5. package/dist/adapters/node.d.ts +11 -0
  6. package/dist/adapters/node.js +24 -0
  7. package/dist/app.d.ts +223 -1
  8. package/dist/app.js +358 -8
  9. package/dist/asyncapi.d.ts +98 -0
  10. package/dist/asyncapi.js +212 -0
  11. package/dist/auto-ban.d.ts +205 -0
  12. package/dist/auto-ban.js +222 -0
  13. package/dist/bot-guard.d.ts +209 -0
  14. package/dist/bot-guard.js +291 -0
  15. package/dist/cli.d.ts +8 -0
  16. package/dist/cli.js +88 -4
  17. package/dist/concurrency-limit.d.ts +135 -0
  18. package/dist/concurrency-limit.js +254 -0
  19. package/dist/docs.d.ts +57 -6
  20. package/dist/docs.js +34 -3
  21. package/dist/errors.d.ts +20 -0
  22. package/dist/errors.js +27 -0
  23. package/dist/fetch-guard.js +4 -0
  24. package/dist/fetch-resilience.d.ts +295 -0
  25. package/dist/fetch-resilience.js +485 -0
  26. package/dist/geo-block.d.ts +184 -0
  27. package/dist/geo-block.js +153 -0
  28. package/dist/hashing.d.ts +2 -1
  29. package/dist/hashing.js +12 -1
  30. package/dist/http-signatures.d.ts +303 -0
  31. package/dist/http-signatures.js +782 -0
  32. package/dist/idempotency.d.ts +204 -0
  33. package/dist/idempotency.js +341 -0
  34. package/dist/index.d.ts +38 -4
  35. package/dist/index.js +18 -1
  36. package/dist/ip-reputation.d.ts +198 -0
  37. package/dist/ip-reputation.js +253 -0
  38. package/dist/jwk.d.ts +15 -0
  39. package/dist/jwk.js +24 -2
  40. package/dist/load-shedding.d.ts +5 -0
  41. package/dist/logger.js +6 -2
  42. package/dist/metrics.d.ts +208 -0
  43. package/dist/metrics.js +452 -0
  44. package/dist/middleware.js +0 -10
  45. package/dist/mtls.d.ts +266 -0
  46. package/dist/mtls.js +488 -0
  47. package/dist/multipart.js +1 -1
  48. package/dist/openapi-diff.d.ts +79 -0
  49. package/dist/openapi-diff.js +246 -0
  50. package/dist/openapi.js +4 -1
  51. package/dist/pagination.d.ts +210 -0
  52. package/dist/pagination.js +353 -0
  53. package/dist/rate-limit-redis.d.ts +8 -0
  54. package/dist/rate-limit-redis.js +8 -0
  55. package/dist/request-decompression.d.ts +200 -0
  56. package/dist/request-decompression.js +363 -0
  57. package/dist/response-cache.d.ts +205 -0
  58. package/dist/response-cache.js +374 -0
  59. package/dist/router.d.ts +22 -0
  60. package/dist/router.js +64 -7
  61. package/dist/safe-redirect.d.ts +2 -2
  62. package/dist/safe-redirect.js +3 -8
  63. package/dist/sbom.cdx.json +9 -9
  64. package/dist/sbom.spdx.json +5 -5
  65. package/dist/scheduler.d.ts +315 -0
  66. package/dist/scheduler.js +546 -0
  67. package/dist/security.d.ts +27 -7
  68. package/dist/security.js +27 -7
  69. package/dist/session.js +3 -3
  70. package/dist/types.d.ts +33 -0
  71. package/dist/waf.d.ts +213 -0
  72. package/dist/waf.js +334 -0
  73. package/dist/webhook-delivery.d.ts +263 -0
  74. package/dist/webhook-delivery.js +311 -0
  75. package/dist/websocket.d.ts +52 -0
  76. package/dist/websocket.js +13 -0
  77. package/package.json +76 -2
@@ -0,0 +1,246 @@
1
+ /**
2
+ * `@daloyjs/core/openapi-diff` — pure, dependency-free OpenAPI 3.x diffing.
3
+ *
4
+ * Compares two OpenAPI documents (a published *baseline* and a freshly
5
+ * generated *current* spec) and classifies every structural change as either
6
+ * **breaking** (a consumer relying on the baseline could now fail) or
7
+ * **non-breaking** (purely additive / informational). This is the engine
8
+ * behind the `daloy diff` CLI command and the `verify:breaking-changes` CI
9
+ * gate, answering the single question a contract-first framework should make
10
+ * trivial: *"did this change break my published API?"*
11
+ *
12
+ * The implementation walks plain JSON and never imports a schema validator or
13
+ * any runtime dependency, so it can run in any environment that can read two
14
+ * JSON files.
15
+ *
16
+ * @module
17
+ * @since 0.37.0
18
+ */
19
+ /** HTTP methods recognized on an OpenAPI Path Item Object. */
20
+ const HTTP_METHODS = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
21
+ /** Narrow an unknown value to a plain object (non-null, non-array). */
22
+ function isObject(value) {
23
+ return typeof value === "object" && value !== null && !Array.isArray(value);
24
+ }
25
+ /** Extract the `paths` object from a document, tolerating malformed input. */
26
+ function pathsOf(doc) {
27
+ if (isObject(doc) && isObject(doc.paths))
28
+ return doc.paths;
29
+ return {};
30
+ }
31
+ /** Extract the OpenAPI Operation Objects keyed by HTTP method for a path item. */
32
+ function operationsOf(pathItem) {
33
+ const ops = new Map();
34
+ if (!isObject(pathItem))
35
+ return ops;
36
+ for (const method of HTTP_METHODS) {
37
+ const op = pathItem[method];
38
+ if (isObject(op))
39
+ ops.set(method, op);
40
+ }
41
+ return ops;
42
+ }
43
+ /** Build a `${in}:${name}` keyed map of an operation's parameter objects. */
44
+ function parametersOf(op) {
45
+ const out = new Map();
46
+ const params = op.parameters;
47
+ if (!Array.isArray(params))
48
+ return out;
49
+ for (const p of params) {
50
+ if (!isObject(p))
51
+ continue;
52
+ const name = typeof p.name === "string" ? p.name : "";
53
+ const loc = typeof p.in === "string" ? p.in : "";
54
+ if (name === "")
55
+ continue;
56
+ out.set(`${loc}:${name}`, p);
57
+ }
58
+ return out;
59
+ }
60
+ /** True when a parameter object is marked `required: true`. */
61
+ function isRequiredParam(p) {
62
+ return p.required === true;
63
+ }
64
+ /** True when an operation's request body is marked `required: true`. */
65
+ function requestBodyRequired(op) {
66
+ return isObject(op.requestBody) && op.requestBody.required === true;
67
+ }
68
+ /**
69
+ * Compare a baseline OpenAPI document against a current one and classify the
70
+ * differences. The comparison is intentionally conservative: anything that
71
+ * could cause a request that succeeded against the baseline to fail against
72
+ * the current spec is reported as **breaking**; additive and metadata-only
73
+ * changes are reported as **non-breaking**.
74
+ *
75
+ * Detected breaking changes:
76
+ * - a path or operation (HTTP method) present in the baseline is removed;
77
+ * - a documented response status code is removed from an operation;
78
+ * - a new `required` parameter is added to an existing operation;
79
+ * - an existing optional parameter becomes `required`;
80
+ * - an operation's request body becomes required when it was not.
81
+ *
82
+ * Detected non-breaking changes:
83
+ * - new paths, operations, response codes, or optional parameters;
84
+ * - a parameter is removed (the server no longer reads it);
85
+ * - an operation becomes `deprecated`;
86
+ * - the document `info.version` changes.
87
+ *
88
+ * @param baseline - The previously published OpenAPI document (JSON).
89
+ * @param current - The freshly generated OpenAPI document (JSON).
90
+ * @returns Structured lists of breaking and non-breaking changes.
91
+ * @since 0.37.0
92
+ */
93
+ export function diffOpenAPI(baseline, current) {
94
+ const breaking = [];
95
+ const nonBreaking = [];
96
+ const basePaths = pathsOf(baseline);
97
+ const curPaths = pathsOf(current);
98
+ // info.version change (informational).
99
+ const baseVersion = isObject(baseline) && isObject(baseline.info) ? baseline.info.version : undefined;
100
+ const curVersion = isObject(current) && isObject(current.info) ? current.info.version : undefined;
101
+ if (baseVersion !== curVersion) {
102
+ nonBreaking.push({
103
+ severity: "non-breaking",
104
+ kind: "info.version.changed",
105
+ location: "info.version",
106
+ detail: `version changed from ${JSON.stringify(baseVersion)} to ${JSON.stringify(curVersion)}`,
107
+ });
108
+ }
109
+ for (const [path, baseItem] of Object.entries(basePaths)) {
110
+ const curItem = curPaths[path];
111
+ const baseOps = operationsOf(baseItem);
112
+ const curOps = operationsOf(curItem);
113
+ for (const [method, baseOp] of baseOps) {
114
+ const where = `${method.toUpperCase()} ${path}`;
115
+ const curOp = curOps.get(method);
116
+ if (!curOp) {
117
+ breaking.push({
118
+ severity: "breaking",
119
+ kind: "operation.removed",
120
+ location: where,
121
+ detail: `operation ${where} was removed`,
122
+ });
123
+ continue;
124
+ }
125
+ diffResponses(baseOp, curOp, where, breaking);
126
+ diffParameters(baseOp, curOp, where, breaking, nonBreaking);
127
+ diffRequestBody(baseOp, curOp, where, breaking);
128
+ if (curOp.deprecated === true && baseOp.deprecated !== true) {
129
+ nonBreaking.push({
130
+ severity: "non-breaking",
131
+ kind: "operation.deprecated",
132
+ location: where,
133
+ detail: `operation ${where} is now deprecated`,
134
+ });
135
+ }
136
+ }
137
+ // Operations added to an existing path.
138
+ for (const [method] of curOps) {
139
+ if (!baseOps.has(method)) {
140
+ nonBreaking.push({
141
+ severity: "non-breaking",
142
+ kind: "operation.added",
143
+ location: `${method.toUpperCase()} ${path}`,
144
+ detail: `operation ${method.toUpperCase()} ${path} was added`,
145
+ });
146
+ }
147
+ }
148
+ }
149
+ // Entirely new paths.
150
+ for (const [path, curItem] of Object.entries(curPaths)) {
151
+ if (path in basePaths)
152
+ continue;
153
+ for (const [method] of operationsOf(curItem)) {
154
+ nonBreaking.push({
155
+ severity: "non-breaking",
156
+ kind: "operation.added",
157
+ location: `${method.toUpperCase()} ${path}`,
158
+ detail: `operation ${method.toUpperCase()} ${path} was added`,
159
+ });
160
+ }
161
+ }
162
+ return { breaking, nonBreaking };
163
+ }
164
+ /** Report response status codes that existed in the baseline but were removed. */
165
+ function diffResponses(baseOp, curOp, where, breaking) {
166
+ const baseResponses = isObject(baseOp.responses) ? baseOp.responses : {};
167
+ const curResponses = isObject(curOp.responses) ? curOp.responses : {};
168
+ for (const status of Object.keys(baseResponses)) {
169
+ if (!(status in curResponses)) {
170
+ breaking.push({
171
+ severity: "breaking",
172
+ kind: "response.removed",
173
+ location: `${where} → ${status}`,
174
+ detail: `response ${status} was removed from ${where}`,
175
+ });
176
+ }
177
+ }
178
+ }
179
+ /** Report parameter additions, removals, and requirement tightening. */
180
+ function diffParameters(baseOp, curOp, where, breaking, nonBreaking) {
181
+ const baseParams = parametersOf(baseOp);
182
+ const curParams = parametersOf(curOp);
183
+ for (const [key, baseParam] of baseParams) {
184
+ const curParam = curParams.get(key);
185
+ if (!curParam) {
186
+ nonBreaking.push({
187
+ severity: "non-breaking",
188
+ kind: "parameter.removed",
189
+ location: `${where} (${key})`,
190
+ detail: `parameter ${key} was removed from ${where}`,
191
+ });
192
+ continue;
193
+ }
194
+ if (!isRequiredParam(baseParam) && isRequiredParam(curParam)) {
195
+ breaking.push({
196
+ severity: "breaking",
197
+ kind: "parameter.required.tightened",
198
+ location: `${where} (${key})`,
199
+ detail: `parameter ${key} on ${where} is now required`,
200
+ });
201
+ }
202
+ }
203
+ for (const [key, curParam] of curParams) {
204
+ if (baseParams.has(key))
205
+ continue;
206
+ if (isRequiredParam(curParam)) {
207
+ breaking.push({
208
+ severity: "breaking",
209
+ kind: "parameter.required.added",
210
+ location: `${where} (${key})`,
211
+ detail: `new required parameter ${key} was added to ${where}`,
212
+ });
213
+ }
214
+ else {
215
+ nonBreaking.push({
216
+ severity: "non-breaking",
217
+ kind: "parameter.added",
218
+ location: `${where} (${key})`,
219
+ detail: `new optional parameter ${key} was added to ${where}`,
220
+ });
221
+ }
222
+ }
223
+ }
224
+ /** Report a request body that became required when it previously was not. */
225
+ function diffRequestBody(baseOp, curOp, where, breaking) {
226
+ if (!requestBodyRequired(baseOp) && requestBodyRequired(curOp)) {
227
+ breaking.push({
228
+ severity: "breaking",
229
+ kind: "requestBody.required.added",
230
+ location: where,
231
+ detail: `request body on ${where} is now required`,
232
+ });
233
+ }
234
+ }
235
+ /**
236
+ * Convenience predicate over {@link diffOpenAPI}: `true` when the current
237
+ * document introduces at least one breaking change versus the baseline.
238
+ *
239
+ * @param baseline - The previously published OpenAPI document (JSON).
240
+ * @param current - The freshly generated OpenAPI document (JSON).
241
+ * @returns `true` if any breaking change was detected.
242
+ * @since 0.37.0
243
+ */
244
+ export function hasBreakingChanges(baseline, current) {
245
+ return diffOpenAPI(baseline, current).breaking.length > 0;
246
+ }
package/dist/openapi.js CHANGED
@@ -107,7 +107,10 @@ function buildOperation(route, path) {
107
107
  ? { description: route.description ?? meta?.description }
108
108
  : {}),
109
109
  ...(mergedTags.length ? { tags: mergedTags } : {}),
110
- ...(route.deprecated ? { deprecated: true } : {}),
110
+ ...(route.deprecated || route.sunset ? { deprecated: true } : {}),
111
+ ...(route.sunset
112
+ ? { "x-sunset": route.sunset }
113
+ : {}),
111
114
  };
112
115
  const parameters = [];
113
116
  if (path) {
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Cursor pagination helpers for DaloyJS.
3
+ *
4
+ * Contract-first list endpoints need three things the framework did not yet
5
+ * ship: an **opaque cursor** the client can echo back without depending on its
6
+ * internals, an **RFC 8288 `Link` header** advertising the `next` / `prev` /
7
+ * `first` pages, and **OpenAPI parameter wiring** so the `cursor` / `limit`
8
+ * query parameters appear in the generated spec and typed client. This module
9
+ * provides all three with zero runtime dependencies:
10
+ *
11
+ * - {@link encodeCursor} / {@link decodeCursor} — base64url-encode an arbitrary
12
+ * JSON-serializable payload (typically the sort key of the last row) into an
13
+ * opaque, URL-safe token, and decode it back with prototype-pollution-safe
14
+ * parsing and a hard size cap.
15
+ * - {@link buildLinkHeader} / {@link buildPageLinks} — assemble a Web-standard
16
+ * `Link` header, with CRLF / angle-bracket header-injection guards baked in.
17
+ * - {@link paginationQuery} — a Standard Schema validator for the `cursor` +
18
+ * `limit` query parameters that both validates at runtime (clamping `limit`
19
+ * to a safe range) **and** advertises itself to the OpenAPI generator via a
20
+ * `toJSONSchema()` method, so `request: { query: paginationQuery() }` wires
21
+ * the parameters into the contract with no extra code.
22
+ *
23
+ * Everything here is built on Web-standard `URL` / `Request` and `btoa` /
24
+ * `atob`, so it runs unchanged on Node, Bun, Deno, Cloudflare Workers, and
25
+ * Vercel Edge.
26
+ *
27
+ * @module
28
+ * @since 0.37.0
29
+ */
30
+ import type { StandardSchemaV1 } from "./schema.js";
31
+ /**
32
+ * Hard cap on the length of an encoded cursor string accepted by
33
+ * {@link decodeCursor}. Bounds the work an attacker can force by sending a
34
+ * giant `cursor` query parameter. 4 KiB is far larger than any legitimate
35
+ * sort-key payload.
36
+ */
37
+ export declare const MAX_CURSOR_LENGTH = 4096;
38
+ /**
39
+ * Encode an arbitrary JSON-serializable value into an opaque, URL-safe cursor
40
+ * token (base64url, no padding).
41
+ *
42
+ * The token is **opaque, not secret**: it is encoded, not encrypted or signed.
43
+ * Never trust a decoded cursor for authorization — always re-scope the
44
+ * underlying query by the authenticated principal on the server. Put only the
45
+ * data you need to resume a scan (e.g. `{ id, createdAt }`) inside it.
46
+ *
47
+ * @param payload - Any JSON-serializable value (object, array, string, …).
48
+ * @returns A base64url cursor string safe to place in a URL or `Link` header.
49
+ * @throws {TypeError} If `payload` cannot be JSON-serialized (e.g. a `BigInt`
50
+ * or a circular structure).
51
+ * @since 0.37.0
52
+ */
53
+ export declare function encodeCursor(payload: unknown): string;
54
+ /**
55
+ * Decode an opaque cursor produced by {@link encodeCursor} back into its
56
+ * original value.
57
+ *
58
+ * Parsing is hardened: the input length is capped at {@link MAX_CURSOR_LENGTH},
59
+ * decoding rejects malformed base64url, and any `__proto__` / `constructor` /
60
+ * `prototype` keys in the decoded object graph are stripped (prototype-
61
+ * pollution defense, mirroring the core body parsers).
62
+ *
63
+ * @typeParam T - The expected shape of the decoded payload (caller-asserted).
64
+ * @param cursor - The opaque cursor string from the request.
65
+ * @returns The decoded payload.
66
+ * @throws {BadRequestError} If the cursor is missing, over-long, or malformed —
67
+ * a `400` so a tampered cursor surfaces as a client error, not a `500`.
68
+ * @since 0.37.0
69
+ */
70
+ export declare function decodeCursor<T = unknown>(cursor: string): T;
71
+ /** A single web link for an RFC 8288 `Link` header. */
72
+ export interface PaginationLink {
73
+ /** Target URI-Reference (the `<...>` portion). */
74
+ url: string;
75
+ /** Relation type, e.g. `"next"`, `"prev"`, `"first"`, `"last"`. */
76
+ rel: string;
77
+ /** Optional human-readable `title` parameter. */
78
+ title?: string;
79
+ }
80
+ /**
81
+ * Serialize a list of links into a single RFC 8288 `Link` header value.
82
+ *
83
+ * Each entry renders as `<url>; rel="rel"` (plus `; title="…"` when present).
84
+ * URLs containing control characters, `<`, or `>` and rel/title values
85
+ * containing control characters, `"`, or `\` are rejected — a structural
86
+ * defense against `Link`-header / response-splitting injection.
87
+ *
88
+ * @param links - The links to emit. An empty array yields an empty string.
89
+ * @returns The comma-joined `Link` header value.
90
+ * @throws {Error} If any URL or token contains forbidden characters.
91
+ * @since 0.37.0
92
+ */
93
+ export declare function buildLinkHeader(links: readonly PaginationLink[]): string;
94
+ /** Options for {@link buildPageLinks}. */
95
+ export interface PageLinkOptions {
96
+ /** The current request URL (string or `URL`); other query params are kept. */
97
+ url: string | URL;
98
+ /** Query-parameter name carrying the cursor. Default: `"cursor"`. */
99
+ cursorParam?: string;
100
+ /** Opaque cursor for the next page, or `null`/`undefined` to omit `next`. */
101
+ next?: string | null;
102
+ /** Opaque cursor for the previous page, or `null`/`undefined` to omit `prev`. */
103
+ prev?: string | null;
104
+ /**
105
+ * Emit a `rel="first"` link (the current URL with the cursor param removed).
106
+ * Default: `false`.
107
+ */
108
+ first?: boolean;
109
+ /** Extra links appended verbatim (e.g. a `rel="last"`). */
110
+ extraLinks?: readonly PaginationLink[];
111
+ }
112
+ /** Result of {@link buildPageLinks}. */
113
+ export interface PageLinks {
114
+ /** The structured links, ready for {@link buildLinkHeader} or a JSON body. */
115
+ links: PaginationLink[];
116
+ /** The serialized RFC 8288 `Link` header value (empty when no links). */
117
+ linkHeader: string;
118
+ /** Convenience map of the computed page URLs. */
119
+ urls: {
120
+ self: string;
121
+ next?: string;
122
+ prev?: string;
123
+ first?: string;
124
+ };
125
+ }
126
+ /**
127
+ * Build the `next` / `prev` / `first` page URLs for a list response by cloning
128
+ * the current request URL and swapping its cursor query parameter, then
129
+ * serialize them into an RFC 8288 `Link` header.
130
+ *
131
+ * All other query parameters (filters, `limit`, …) are preserved, so the
132
+ * generated links are drop-in "give me the same query, next page" URLs.
133
+ *
134
+ * @example
135
+ * ```ts
136
+ * const { linkHeader } = buildPageLinks({
137
+ * url: ctx.request.url,
138
+ * next: nextCursor, // from encodeCursor(...)
139
+ * prev: prevCursor,
140
+ * first: true,
141
+ * });
142
+ * set.headers.set("Link", linkHeader);
143
+ * ```
144
+ *
145
+ * @param opts - Current URL plus the cursors to advertise.
146
+ * @returns The structured links, the `Link` header string, and the page URLs.
147
+ * @since 0.37.0
148
+ */
149
+ export declare function buildPageLinks(opts: PageLinkOptions): PageLinks;
150
+ /** Options for {@link paginationQuery}. */
151
+ export interface PaginationQueryOptions {
152
+ /** Query-parameter name for the cursor. Default: `"cursor"`. */
153
+ cursorParam?: string;
154
+ /** Query-parameter name for the page size. Default: `"limit"`. */
155
+ limitParam?: string;
156
+ /** Default page size applied when `limit` is omitted. Default: `min(20, maxLimit)`. */
157
+ defaultLimit?: number;
158
+ /** Minimum accepted page size. Default: `1`. */
159
+ minLimit?: number;
160
+ /** Maximum accepted page size (also caps over-large requests). Default: `100`. */
161
+ maxLimit?: number;
162
+ }
163
+ /** Validated output of {@link paginationQuery}. */
164
+ export interface PaginationParams {
165
+ /** The resolved page size, clamped to `[minLimit, maxLimit]`. */
166
+ limit: number;
167
+ /** The opaque cursor, if the client supplied one. */
168
+ cursor?: string;
169
+ }
170
+ /**
171
+ * A Standard Schema for cursor-pagination query parameters that also carries a
172
+ * `toJSONSchema()` method so the OpenAPI generator wires the `cursor` and
173
+ * `limit` parameters into the contract automatically.
174
+ */
175
+ export interface PaginationQuerySchema extends StandardSchemaV1<Record<string, unknown>, PaginationParams> {
176
+ /** Used by the OpenAPI generator to emit the query parameters. */
177
+ toJSONSchema(): Record<string, unknown>;
178
+ }
179
+ /**
180
+ * Build a Standard Schema validator for cursor-pagination query parameters.
181
+ *
182
+ * Use it as a route's `request.query`. At runtime it parses and validates
183
+ * `limit` (coerced from its string query value to an integer and clamped to
184
+ * `[minLimit, maxLimit]`, defaulting to `defaultLimit` when absent) and passes
185
+ * `cursor` through as an optional opaque string. Because it also exposes
186
+ * `toJSONSchema()`, the same call wires both parameters into the generated
187
+ * OpenAPI document and typed client — no duplicate parameter declarations.
188
+ *
189
+ * @example
190
+ * ```ts
191
+ * app.route({
192
+ * method: "GET",
193
+ * path: "/books",
194
+ * operationId: "listBooks",
195
+ * request: { query: paginationQuery({ defaultLimit: 25, maxLimit: 100 }) },
196
+ * responses: { 200: { description: "ok", body: pageSchema } },
197
+ * handler: async ({ query }) => {
198
+ * const { limit, cursor } = query; // fully typed + validated
199
+ * // ...
200
+ * },
201
+ * });
202
+ * ```
203
+ *
204
+ * @param opts - Parameter names and page-size bounds.
205
+ * @returns A Standard Schema usable as `request.query`.
206
+ * @throws {Error} If the configured bounds are not positive integers or are
207
+ * inconsistent (`minLimit > maxLimit`, `defaultLimit` out of range).
208
+ * @since 0.37.0
209
+ */
210
+ export declare function paginationQuery(opts?: PaginationQueryOptions): PaginationQuerySchema;