@kubb/plugin-axios 5.9.0-canary.20261024T172532 → 5.9.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.
@@ -162,16 +162,19 @@ export type ContentType = string | {
162
162
  * A Standard Schema validator (zod, valibot, arktype) that parses a value before it is sent or after
163
163
  * it is received. `runValidator` runs it through `validateStandardSchema`. Wired through the per-call
164
164
  * `validator.request` / `validator.response` / `validator.error` hooks (`error` runs on the error body when a
165
- * non-2xx call does not throw).
165
+ * non-2xx call does not throw), and `validator.path` / `validator.query` / `validator.headers` for the
166
+ * request params.
166
167
  */
167
168
  export type Validator<T = unknown> = StandardSchemaValidator<T>;
168
169
  /**
169
- * The failing body and the call it came from, handed to `onValidationError` alongside the `ParseError`.
170
- * `direction` says which slot rejected it: the request body, the success body, or the error body.
170
+ * The failing value and the call it came from, handed to `onValidationError` alongside the `ParseError`.
171
+ * `direction` says which side rejected it: the request, the success body, or the error body. `source`
172
+ * says which part of it: the body, or one of the request param groups.
171
173
  */
172
174
  export type ValidationErrorContext = {
173
175
  value: unknown;
174
176
  direction: 'request' | 'response' | 'error';
177
+ source: 'body' | 'path' | 'query' | 'headers';
175
178
  method?: string;
176
179
  url?: string;
177
180
  status?: number;
@@ -194,6 +197,7 @@ export type Auth = {
194
197
  scheme?: 'bearer' | 'basic';
195
198
  name?: string;
196
199
  in?: 'header' | 'query' | 'cookie';
200
+ scopes?: Array<string>;
197
201
  };
198
202
  /**
199
203
  * The raw token a consumer returns for a scheme (or `user:password` for basic), or `undefined` to skip it.
@@ -233,6 +237,9 @@ export type RequestConfig<TBody = unknown, TRequest = AxiosRequestConfig, TRespo
233
237
  codecs?: Record<string, Codec>;
234
238
  validator?: {
235
239
  request?: Validator;
240
+ path?: Validator;
241
+ query?: Validator;
242
+ headers?: Validator;
236
243
  response?: Validator;
237
244
  error?: Validator;
238
245
  };
@@ -291,6 +298,7 @@ export type Interceptors<TRequest = AxiosRequestConfig, TResponse = AxiosRespons
291
298
  response: InterceptorChannel<AxiosResponse, TRequest, TResponse>;
292
299
  error: InterceptorChannel<AxiosError, TRequest, TResponse>;
293
300
  };
301
+ export type UrlConfig = Pick<RequestConfig, 'baseURL' | 'url' | 'path' | 'query' | 'params' | 'serializer' | 'styles'>;
294
302
  /**
295
303
  * A client instance: the callable send plus configuration, interceptors, and an isolated
296
304
  * `createClient` factory.
@@ -299,7 +307,7 @@ export type ClientInstance<TRequest = AxiosRequestConfig, TResponse = AxiosRespo
299
307
  <TBody = unknown>(config: RequestConfig<TBody, TRequest, TResponse>): Promise<CallResult<TRequest, TResponse>>;
300
308
  getConfig: () => ClientConfig;
301
309
  setConfig: (config: ClientConfig) => ClientConfig;
302
- getUrl: <TBody = unknown>(config: RequestConfig<TBody, TRequest, TResponse>) => string;
310
+ getUrl: (config: UrlConfig) => string;
303
311
  interceptors: Interceptors<TRequest, TResponse>;
304
312
  createClient: (config?: ClientConfig) => ClientInstance<TRequest, TResponse>;
305
313
  };
@@ -48,4 +48,18 @@ export declare class ParseError extends Error {
48
48
  * const pet = await validateStandardSchema(PetSchema, rawData)
49
49
  */
50
50
  export declare function validateStandardSchema<TOutput>(schema: StandardSchemaValidator<TOutput>, value: unknown): Promise<TOutput>;
51
+ /**
52
+ * Wraps an object schema so its keys match case-insensitively.
53
+ * Keys the schema does not declare pass through untouched.
54
+ * The declared keys are validated with `schema` under their declared names.
55
+ * Every key, and every issue path, keeps the casing the caller used.
56
+ *
57
+ * @param schema Standard Schema-compatible object schema.
58
+ * @param entries The record of keys the passed object schema validates.
59
+ * @example
60
+ * ```ts
61
+ * validator: { headers: toHeadersStandardSchema(putEventHeadersSchema, putEventHeadersSchema.shape) }
62
+ * ```
63
+ */
64
+ export declare function toHeadersStandardSchema(schema: StandardSchemaValidator, entries: Record<string, unknown>): StandardSchemaValidator;
51
65
  //#endregion
package/docs/options.md CHANGED
@@ -10,7 +10,7 @@ Pass these options to `pluginAxios()`. Shared options link to [Shared plugin opt
10
10
  | [`group`](https://kubb.dev/docs/5.x/reference/plugin-options#group) | Split output by tag or path: `type`, `name`. | None |
11
11
  | [`baseURL`](#baseurl) | Base URL prepended to every request. | None |
12
12
  | [`throwOnErrorDefault`](#throwonerrordefault) | Default error behavior and return type for generated operations. | `true` |
13
- | [`validator`](#validator) | Validate request and response bodies with Zod. | `false` |
13
+ | [`validator`](#validator) | Validate request bodies, params, and responses with Zod. | `false` |
14
14
  | [`sdk`](#sdk) | Emit a class-based SDK instead of standalone functions. | None |
15
15
  | [`returnType`](#returntype) | Shape of the value a generated call resolves to. | `'full'` |
16
16
  | [`include`](https://kubb.dev/docs/5.x/reference/plugin-options#include) | Keep only operations that match. | None |
@@ -48,13 +48,13 @@ if (result.status !== 200) console.error(result.status, result.error)
48
48
 
49
49
  ### validator
50
50
 
51
- Validates request and response bodies with schemas from `@kubb/plugin-zod`, so add `pluginZod()` too. An invalid body makes the generated function throw a `ParseError`.
51
+ Validates request bodies, path, query, and header params, and response bodies with schemas from `@kubb/plugin-zod`, so add `pluginZod()` too. An invalid value makes the generated function throw a `ParseError`.
52
52
 
53
- | | |
54
- | -------- | --------------------------------------------------------- |
55
- | Type | `false \| 'zod' \| { request?: 'zod'; response?: 'zod' }` |
56
- | Required | `false` |
57
- | Default | `false` |
53
+ | | |
54
+ | -------- | ------------------------------------------------------------------------- |
55
+ | Type | `false \| 'zod' \| { request?: 'zod'; params?: 'zod'; response?: 'zod' }` |
56
+ | Required | `false` |
57
+ | Default | `false` |
58
58
 
59
59
  ::field-group
60
60
 
@@ -66,8 +66,8 @@ Default value. Skips validation and casts the response to the generated type.
66
66
  Validates the success response body and, when a non-2xx call does not throw, the error body.
67
67
  :::
68
68
 
69
- :::field{name="{ request?: 'zod', response?: 'zod' }"}
70
- Enables validation separately for requests and responses.
69
+ :::field{name="{ request?: 'zod', params?: 'zod', response?: 'zod' }"}
70
+ Enables validation separately for request bodies, params, and responses.
71
71
  :::
72
72
 
73
73
  ::
@@ -84,11 +84,17 @@ export default defineConfig({
84
84
  plugins: [
85
85
  pluginTs(),
86
86
  pluginZod(),
87
- pluginAxios({ validator: { request: 'zod', response: 'zod' } }),
87
+ pluginAxios({ validator: { request: 'zod', params: 'zod', response: 'zod' } }),
88
88
  ],
89
89
  })
90
90
  ```
91
91
 
92
+ `params` runs each group through its generated `<operation>PathSchema`, `QuerySchema`, or `HeadersSchema`, and sends what the schema returns. A codec param such as a `Date` goes out in its wire form, and an invalid param fails before the request. Values added by `auth` are not validated.
93
+
94
+ Only the params your OpenAPI spec declares are sent. Path and query keys the spec does not declare are stripped, so a key such as an undocumented filter or a cache-buster never reaches the URL. Declare it in the spec, or leave `params` off. Headers are the exception: client-level `headers` (such as `Authorization` or tracing headers) are merged with the call's headers before validation, so headers the spec does not declare pass through unchanged, and declared header params match in any casing.
95
+
96
+ Pair `request` and `params` with `pluginZod({ inferred: 'direction' })` when the schemas use codecs, so the request types accept the domain values (such as a `Date`) that the validators encode. See [`inferred`](https://kubb.dev/extensions/kubb-labs/plugin-zod/options#inferred).
97
+
92
98
  ### sdk
93
99
 
94
100
  Generates a class-based SDK. Each instance takes its own client config, so environments can use separate clients. Leave `sdk` unset to keep the standalone functions that query plugins call.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubb/plugin-axios",
3
- "version": "5.9.0-canary.20261024T172532",
3
+ "version": "5.9.0",
4
4
  "description": "Generate a slim, type-safe HTTP client with Kubb based on axios.",
5
5
  "keywords": [
6
6
  "api-client",
@@ -80,12 +80,12 @@
80
80
  "registry": "https://registry.npmjs.org/"
81
81
  },
82
82
  "dependencies": {
83
- "@kubb/plugin-ts": "^5.2.0-canary.20261024T172532",
84
- "@kubb/plugin-zod": "^5.7.0-canary.20261024T172532"
83
+ "@kubb/plugin-ts": "^5.1.0",
84
+ "@kubb/plugin-zod": "^5.7.0"
85
85
  },
86
86
  "devDependencies": {
87
- "@internals/client": "0.1.0-canary.20261024T172532",
88
- "@internals/shared": "0.1.0-canary.20261024T172532",
87
+ "@internals/client": "0.0.0",
88
+ "@internals/shared": "0.0.0",
89
89
  "axios": "^1.20.0",
90
90
  "kubb": "^5.13.0",
91
91
  "typescript": "^7.0.2"
@@ -165,17 +165,20 @@ export type ContentType = string | { request?: string; response?: string }
165
165
  * A Standard Schema validator (zod, valibot, arktype) that parses a value before it is sent or after
166
166
  * it is received. `runValidator` runs it through `validateStandardSchema`. Wired through the per-call
167
167
  * `validator.request` / `validator.response` / `validator.error` hooks (`error` runs on the error body when a
168
- * non-2xx call does not throw).
168
+ * non-2xx call does not throw), and `validator.path` / `validator.query` / `validator.headers` for the
169
+ * request params.
169
170
  */
170
171
  export type Validator<T = unknown> = StandardSchemaValidator<T>
171
172
 
172
173
  /**
173
- * The failing body and the call it came from, handed to `onValidationError` alongside the `ParseError`.
174
- * `direction` says which slot rejected it: the request body, the success body, or the error body.
174
+ * The failing value and the call it came from, handed to `onValidationError` alongside the `ParseError`.
175
+ * `direction` says which side rejected it: the request, the success body, or the error body. `source`
176
+ * says which part of it: the body, or one of the request param groups.
175
177
  */
176
178
  export type ValidationErrorContext = {
177
179
  value: unknown
178
180
  direction: 'request' | 'response' | 'error'
181
+ source: 'body' | 'path' | 'query' | 'headers'
179
182
  method?: string
180
183
  url?: string
181
184
  status?: number
@@ -196,6 +199,7 @@ export type Auth = {
196
199
  scheme?: 'bearer' | 'basic'
197
200
  name?: string
198
201
  in?: 'header' | 'query' | 'cookie'
202
+ scopes?: Array<string>
199
203
  }
200
204
 
201
205
  /**
@@ -237,7 +241,7 @@ export type RequestConfig<TBody = unknown, TRequest = AxiosRequestConfig, TRespo
237
241
  transport?: AxiosInstance
238
242
  serializer?: Serializers
239
243
  codecs?: Record<string, Codec>
240
- validator?: { request?: Validator; response?: Validator; error?: Validator }
244
+ validator?: { request?: Validator; path?: Validator; query?: Validator; headers?: Validator; response?: Validator; error?: Validator }
241
245
  onValidationError?: ValidationErrorHandler
242
246
  security?: Array<Auth>
243
247
  auth?: AuthResolver
@@ -307,6 +311,8 @@ export type Interceptors<TRequest = AxiosRequestConfig, TResponse = AxiosRespons
307
311
  error: InterceptorChannel<AxiosError, TRequest, TResponse>
308
312
  }
309
313
 
314
+ export type UrlConfig = Pick<RequestConfig, 'baseURL' | 'url' | 'path' | 'query' | 'params' | 'serializer' | 'styles'>
315
+
310
316
  /**
311
317
  * A client instance: the callable send plus configuration, interceptors, and an isolated
312
318
  * `createClient` factory.
@@ -315,7 +321,7 @@ export type ClientInstance<TRequest = AxiosRequestConfig, TResponse = AxiosRespo
315
321
  <TBody = unknown>(config: RequestConfig<TBody, TRequest, TResponse>): Promise<CallResult<TRequest, TResponse>>
316
322
  getConfig: () => ClientConfig
317
323
  setConfig: (config: ClientConfig) => ClientConfig
318
- getUrl: <TBody = unknown>(config: RequestConfig<TBody, TRequest, TResponse>) => string
324
+ getUrl: (config: UrlConfig) => string
319
325
  interceptors: Interceptors<TRequest, TResponse>
320
326
  createClient: (config?: ClientConfig) => ClientInstance<TRequest, TResponse>
321
327
  }
@@ -365,9 +371,14 @@ export class ResponseError<TError = unknown, TRequest = AxiosRequestConfig, TRes
365
371
 
366
372
  export type ResponseErrorConfig<TError = unknown> = ResponseError<TError>
367
373
 
374
+ function headerEntries(headers: object): Array<[string, unknown]> {
375
+ if (Array.isArray(headers)) return headers
376
+ return headers instanceof Headers ? [...headers] : Object.entries(headers)
377
+ }
378
+
368
379
  function serializeHeaders(headers: HeadersInit | undefined): Record<string, string> {
369
380
  if (!headers) return {}
370
- const entries = Array.isArray(headers) ? headers : Object.entries(headers)
381
+ const entries = headerEntries(headers)
371
382
  const result: Record<string, string> = {}
372
383
  for (const [key, value] of entries) {
373
384
  if (value === undefined || value === null) continue
@@ -376,8 +387,22 @@ function serializeHeaders(headers: HeadersInit | undefined): Record<string, stri
376
387
  return result
377
388
  }
378
389
 
379
- function mergeHeaders(...sources: Array<HeadersInit | undefined>): Record<string, string> {
380
- return Object.assign({}, ...sources.map(serializeHeaders))
390
+ function collectHeadersCaseInsensitive(...sources: Array<unknown>): Record<string, unknown> {
391
+ const keys = new Map<string, string>()
392
+ const result: Record<string, unknown> = {}
393
+ for (const source of sources) {
394
+ if (!source) continue
395
+ const entries = headerEntries(source)
396
+ for (const [key, value] of entries) {
397
+ if (value === undefined || value === null) continue
398
+ const lowerCase = key.toLowerCase()
399
+ const previous = keys.get(lowerCase)
400
+ if (previous !== undefined) delete result[previous]
401
+ keys.set(lowerCase, key)
402
+ result[key] = value
403
+ }
404
+ }
405
+ return result
381
406
  }
382
407
 
383
408
  function getHeader(headers: Record<string, string>, name: string): string | undefined {
@@ -585,15 +610,29 @@ async function resolveRequest<TBody, TRequest, TResponse>({
585
610
  }): Promise<{ axiosConfig: AxiosRequestConfig; codecs: Record<string, Codec>; throwOnError: boolean }> {
586
611
  const { querySerializer, bodySerializer, pathSerializer } = resolveSerializers({ config, requestConfig })
587
612
  const codecs = { ...config.codecs, ...requestConfig.codecs }
613
+ const onValidationError = requestConfig.onValidationError ?? config.onValidationError
614
+ const validationContext = { direction: 'request', method: requestConfig.method, url: requestConfig.url } as const
615
+
616
+ // Params are encoded before anything reads them: headers before styling, query before auth adds its values.
617
+ const paramValues = {
618
+ path: { ...(requestConfig.path as Record<string, unknown> | undefined) },
619
+ query: { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) },
620
+ headers: collectHeadersCaseInsensitive(config.headers, requestConfig.headers),
621
+ }
622
+ const [validatedPath, validatedQuery, validatedHeaders] = await Promise.all(
623
+ (['path', 'query', 'headers'] as const).map((source) =>
624
+ runValidator({ validator: requestConfig.validator?.[source], value: paramValues[source], context: { ...validationContext, source }, onValidationError }),
625
+ ),
626
+ )
588
627
 
589
- const headers = mergeHeaders(config.headers, applyHeaderStyles(requestConfig.headers as HeadersInit | undefined, requestConfig.styles?.header))
628
+ const headers = serializeHeaders(applyHeaderStyles(validatedHeaders as HeadersInit | undefined, requestConfig.styles?.header))
590
629
  const { request: requestContentTypeOption, response: responseContentType } = resolveContentType(requestConfig.contentType)
591
630
  const requestContentType = requestContentTypeOption ?? getHeader(headers, 'content-type')
592
631
  if (responseContentType && !hasHeader(headers, 'accept')) {
593
632
  headers['Accept'] = responseContentType
594
633
  }
595
634
 
596
- const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
635
+ const query: Record<string, unknown> = { ...(validatedQuery as Record<string, unknown> | undefined) }
597
636
 
598
637
  await resolveAuth({
599
638
  security: requestConfig.security,
@@ -610,8 +649,8 @@ async function resolveRequest<TBody, TRequest, TResponse>({
610
649
  const validatedBody = await runValidator({
611
650
  validator: requestConfig.validator?.request,
612
651
  value: requestConfig.body,
613
- context: { direction: 'request', method: requestConfig.method, url: requestConfig.url },
614
- onValidationError: requestConfig.onValidationError ?? config.onValidationError,
652
+ context: { ...validationContext, source: 'body' },
653
+ onValidationError,
615
654
  })
616
655
  const requestContentTypeBase = baseContentType(requestContentType)
617
656
  const contentCodec = requestContentTypeBase ? codecs[requestContentTypeBase] : undefined
@@ -632,7 +671,7 @@ async function resolveRequest<TBody, TRequest, TResponse>({
632
671
  headers['Content-Type'] = 'application/json'
633
672
  }
634
673
 
635
- const pathParams = (requestConfig.path ?? {}) as Record<string, unknown>
674
+ const pathParams = (validatedPath ?? {}) as Record<string, unknown>
636
675
  const url = (requestConfig.url ?? '').replace(/\{([^{}]+)\}/g, (_, key: string) =>
637
676
  pathSerializer({ name: key, value: pathParams[key], options: requestConfig.styles?.path?.[key] }),
638
677
  )
@@ -700,7 +739,7 @@ async function settleResponse<TRequest, TResponse>({
700
739
  ? await runValidator({
701
740
  validator: validator?.response,
702
741
  value: decoded,
703
- context: { direction: 'response', ...validationContext },
742
+ context: { direction: 'response', source: 'body', ...validationContext },
704
743
  onValidationError,
705
744
  })
706
745
  : undefined
@@ -709,7 +748,7 @@ async function settleResponse<TRequest, TResponse>({
709
748
  : await runValidator({
710
749
  validator: validator?.error,
711
750
  value: decoded,
712
- context: { direction: 'error', ...validationContext },
751
+ context: { direction: 'error', source: 'body', ...validationContext },
713
752
  onValidationError,
714
753
  })
715
754
  return {
@@ -52,3 +52,58 @@ export async function validateStandardSchema<TOutput>(schema: StandardSchemaVali
52
52
  }
53
53
  return (result as { value: TOutput }).value
54
54
  }
55
+
56
+ /**
57
+ * Wraps an object schema so its keys match case-insensitively.
58
+ * Keys the schema does not declare pass through untouched.
59
+ * The declared keys are validated with `schema` under their declared names.
60
+ * Every key, and every issue path, keeps the casing the caller used.
61
+ *
62
+ * @param schema Standard Schema-compatible object schema.
63
+ * @param entries The record of keys the passed object schema validates.
64
+ * @example
65
+ * ```ts
66
+ * validator: { headers: toHeadersStandardSchema(putEventHeadersSchema, putEventHeadersSchema.shape) }
67
+ * ```
68
+ */
69
+ export function toHeadersStandardSchema(schema: StandardSchemaValidator, entries: Record<string, unknown>): StandardSchemaValidator {
70
+ const declared = new Map(Object.keys(entries).map((name) => [name.toLowerCase(), name]))
71
+
72
+ function validate(value: unknown): StandardSchemaResult<unknown> | Promise<StandardSchemaResult<unknown>> {
73
+ if (typeof value !== 'object' || value === null) {
74
+ return { issues: [{ message: 'Expected an object' }] }
75
+ }
76
+
77
+ const originalKeys = new Map<string, string>()
78
+ const declaredValues: Record<string, unknown> = {}
79
+ const undeclaredValues: Record<string, unknown> = {}
80
+ for (const [key, entry] of Object.entries(value)) {
81
+ const name = declared.get(key.toLowerCase())
82
+ if (name === undefined) {
83
+ undeclaredValues[key] = entry
84
+ continue
85
+ }
86
+ originalKeys.set(name, key)
87
+ declaredValues[name] = entry
88
+ }
89
+
90
+ const toOriginal = (key: string) => originalKeys.get(key) ?? key
91
+ const settle = (result: StandardSchemaResult<unknown>): StandardSchemaResult<unknown> => {
92
+ if (result.issues) {
93
+ return {
94
+ issues: result.issues.map((issue): StandardSchemaIssue => ({
95
+ ...issue,
96
+ path: issue.path?.map((segment, index) => (index === 0 && typeof segment === 'string' ? toOriginal(segment) : segment)),
97
+ })),
98
+ }
99
+ }
100
+ const validated = Object.entries((result.value ?? {}) as Record<string, unknown>).map(([key, entry]) => [toOriginal(key), entry])
101
+ return { value: { ...undeclaredValues, ...Object.fromEntries(validated) } }
102
+ }
103
+
104
+ const result = schema['~standard'].validate(declaredValues)
105
+ return result instanceof Promise ? result.then(settle) : settle(result)
106
+ }
107
+
108
+ return { '~standard': { validate } }
109
+ }