hanzio 1.1.0 → 2.0.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 (172) hide show
  1. package/README.md +832 -219
  2. package/dist/api-wrapper/builders.d.ts +15 -3
  3. package/dist/api-wrapper/create-client.d.ts +1 -1
  4. package/dist/api-wrapper/describe.d.ts +16 -0
  5. package/dist/api-wrapper/endpoint.d.ts +18 -0
  6. package/dist/api-wrapper/errors.d.ts +40 -2
  7. package/dist/api-wrapper/headers.d.ts +2 -1
  8. package/dist/api-wrapper/index.d.ts +5 -0
  9. package/dist/api-wrapper/otel.d.ts +44 -0
  10. package/dist/api-wrapper/paginate.d.ts +12 -0
  11. package/dist/api-wrapper/redaction.d.ts +2 -0
  12. package/dist/api-wrapper/shared.d.ts +16 -2
  13. package/dist/api-wrapper/testing.d.ts +23 -0
  14. package/dist/api-wrapper/transport.d.ts +12 -10
  15. package/dist/api-wrapper/types.d.ts +104 -18
  16. package/dist/array/index.d.ts +50 -4
  17. package/dist/cache/createCache.d.ts +31 -0
  18. package/dist/cache/index.d.ts +45 -2
  19. package/dist/color/contrast.d.ts +40 -0
  20. package/dist/color/index.d.ts +2 -0
  21. package/dist/{math → color}/metricsSeriesColors.d.ts +14 -7
  22. package/dist/cool-console-log/index.d.ts +34 -5
  23. package/dist/crypto/encoding.d.ts +18 -0
  24. package/dist/crypto/hash.d.ts +33 -0
  25. package/dist/crypto/index.d.ts +4 -0
  26. package/dist/crypto/sealer.d.ts +25 -0
  27. package/dist/crypto/webhook.d.ts +20 -0
  28. package/dist/date/index.d.ts +46 -2
  29. package/dist/emitter/index.d.ts +21 -0
  30. package/dist/error/index.d.ts +12 -0
  31. package/dist/format/index.d.ts +102 -0
  32. package/dist/function/index.d.ts +60 -0
  33. package/dist/guard/index.d.ts +20 -0
  34. package/dist/i18n/index.d.ts +37 -0
  35. package/dist/index-0zvtcbzy.js +224 -0
  36. package/dist/index-181e6vfw.js +29 -0
  37. package/dist/index-8r2vdzjg.js +87 -0
  38. package/dist/index-hy7kbb99.js +157 -0
  39. package/dist/{src/secrets/loaders.js → index-j1ybktbr.js} +608 -405
  40. package/dist/index-k9yx6bne.js +89 -0
  41. package/dist/index-kj1phgrx.js +169 -0
  42. package/dist/index-mnkcfyx8.js +78 -0
  43. package/dist/{p-queue/index.js → index-pwt0dz63.js} +7 -11
  44. package/dist/index-rp5pqghz.js +157 -0
  45. package/dist/index-srt06xd2.js +15 -0
  46. package/dist/index-wjyb9bsd.js +263 -0
  47. package/dist/index.d.ts +7 -7
  48. package/dist/jwt/index.d.ts +50 -2
  49. package/dist/math/index.d.ts +2 -2
  50. package/dist/math/linearRegressionTrend.d.ts +14 -1
  51. package/dist/math/scale.d.ts +8 -0
  52. package/dist/object/index.d.ts +19 -0
  53. package/dist/p-queue/index.d.ts +1 -1
  54. package/dist/promise/index.d.ts +97 -2
  55. package/dist/secrets/core.d.ts +47 -0
  56. package/dist/secrets/deadline.d.ts +6 -0
  57. package/dist/secrets/environment.d.ts +9 -0
  58. package/dist/secrets/errors.d.ts +11 -0
  59. package/dist/secrets/index.d.ts +13 -25
  60. package/dist/secrets/infisical-cli.d.ts +6 -0
  61. package/dist/secrets/infisical-config.d.ts +17 -0
  62. package/dist/secrets/infisical-session.d.ts +4 -0
  63. package/dist/secrets/infisical.d.ts +8 -13
  64. package/dist/secrets/loaders.d.ts +5 -12
  65. package/dist/secrets/vite.d.ts +7 -3
  66. package/dist/secrets/worker-loader.d.ts +9 -0
  67. package/dist/secrets/worker.d.ts +3 -0
  68. package/dist/sitemap/index.d.ts +48 -6
  69. package/dist/src/api-wrapper/index.js +60 -1100
  70. package/dist/src/api-wrapper/testing.js +20 -0
  71. package/dist/{cool-console-log → src/cool-console-log}/index.js +80 -33
  72. package/dist/src/crypto/index.js +195 -0
  73. package/dist/src/format/index.js +211 -0
  74. package/dist/src/i18n/index.js +36 -0
  75. package/dist/src/index.js +863 -2809
  76. package/dist/src/jwt/index.js +138 -92
  77. package/dist/src/p-queue/index.js +9 -437
  78. package/dist/src/secrets/index.js +261 -1377
  79. package/dist/src/secrets/vite.js +6 -17
  80. package/dist/src/secrets/worker.js +12 -0
  81. package/dist/src/sitemap/index.js +183 -511
  82. package/dist/src/storage/index.js +119 -0
  83. package/dist/src/zod/index.js +347 -174
  84. package/dist/state/index.d.ts +21 -0
  85. package/dist/storage/index.d.ts +58 -0
  86. package/dist/string/index.d.ts +75 -2
  87. package/dist/typedswitch/index.d.ts +37 -26
  88. package/dist/types/index.d.ts +11 -0
  89. package/dist/url/index.d.ts +50 -1
  90. package/dist/zod/index.d.ts +32 -3
  91. package/dist/zod/introspect.d.ts +19 -0
  92. package/dist/zod/jsonToZodSchemaMapper.d.ts +3 -1
  93. package/dist/zod/zodMinMaxFilter.d.ts +22 -3
  94. package/dist/zod/zodToTypeString.d.ts +8 -3
  95. package/package.json +87 -7
  96. package/src/api-wrapper/actions-impl.ts +11 -72
  97. package/src/api-wrapper/builders.ts +84 -52
  98. package/src/api-wrapper/create-client.ts +246 -96
  99. package/src/api-wrapper/describe.ts +157 -0
  100. package/src/api-wrapper/endpoint.ts +26 -0
  101. package/src/api-wrapper/error-reporting.ts +7 -1
  102. package/src/api-wrapper/errors.ts +117 -2
  103. package/src/api-wrapper/headers.ts +19 -1
  104. package/src/api-wrapper/index.ts +5 -0
  105. package/src/api-wrapper/otel.ts +139 -0
  106. package/src/api-wrapper/paginate.ts +28 -0
  107. package/src/api-wrapper/redaction.ts +43 -0
  108. package/src/api-wrapper/shared.ts +21 -2
  109. package/src/api-wrapper/testing.ts +48 -0
  110. package/src/api-wrapper/transport.ts +68 -74
  111. package/src/api-wrapper/types.ts +186 -18
  112. package/src/array/index.ts +195 -42
  113. package/src/cache/createCache.ts +157 -0
  114. package/src/cache/index.ts +235 -83
  115. package/src/color/contrast.ts +163 -0
  116. package/src/color/index.ts +22 -0
  117. package/src/{math → color}/metricsSeriesColors.ts +47 -49
  118. package/src/cool-console-log/index.ts +128 -41
  119. package/src/crypto/encoding.ts +83 -0
  120. package/src/crypto/hash.ts +170 -0
  121. package/src/crypto/index.ts +26 -0
  122. package/src/crypto/sealer.ts +168 -0
  123. package/src/crypto/webhook.ts +86 -0
  124. package/src/date/index.ts +109 -34
  125. package/src/emitter/index.ts +82 -0
  126. package/src/error/index.ts +63 -0
  127. package/src/format/index.ts +389 -0
  128. package/src/function/index.ts +201 -0
  129. package/src/guard/index.ts +80 -0
  130. package/src/i18n/index.ts +125 -0
  131. package/src/index.ts +11 -7
  132. package/src/jwt/index.ts +199 -125
  133. package/src/math/index.ts +5 -10
  134. package/src/math/linearRegressionTrend.ts +33 -8
  135. package/src/math/scale.ts +17 -0
  136. package/src/object/index.ts +99 -0
  137. package/src/p-queue/index.ts +2 -0
  138. package/src/promise/index.ts +304 -9
  139. package/src/secrets/core.ts +348 -0
  140. package/src/secrets/deadline.ts +69 -0
  141. package/src/secrets/environment.ts +69 -0
  142. package/src/secrets/errors.ts +29 -0
  143. package/src/secrets/index.ts +79 -210
  144. package/src/secrets/infisical-cli.ts +99 -0
  145. package/src/secrets/infisical-config.ts +70 -0
  146. package/src/secrets/infisical-session.ts +42 -0
  147. package/src/secrets/infisical.ts +214 -66
  148. package/src/secrets/loaders.ts +24 -48
  149. package/src/secrets/vite.ts +44 -15
  150. package/src/secrets/worker-loader.ts +56 -0
  151. package/src/secrets/worker.ts +4 -0
  152. package/src/sitemap/index.ts +293 -130
  153. package/src/state/index.ts +133 -81
  154. package/src/storage/index.ts +187 -0
  155. package/src/string/index.ts +261 -23
  156. package/src/typedswitch/index.ts +134 -103
  157. package/src/types/index.ts +14 -0
  158. package/src/url/index.ts +146 -1
  159. package/src/zod/index.ts +105 -9
  160. package/src/zod/introspect.ts +103 -0
  161. package/src/zod/jsonToZodSchemaMapper.ts +44 -21
  162. package/src/zod/zodMinMaxFilter.ts +37 -13
  163. package/src/zod/zodToTypeString.ts +184 -181
  164. package/dist/api-wrapper/index.js +0 -250
  165. package/dist/gzip/index.d.ts +0 -2
  166. package/dist/gzip/index.js +0 -16
  167. package/dist/index.js +0 -1823
  168. package/dist/jwt/index.js +0 -19
  169. package/dist/sitemap/index.js +0 -552
  170. package/dist/src/gzip/index.js +0 -16
  171. package/dist/src/secrets/infisical.js +0 -1194
  172. package/dist/zod/index.js +0 -210
@@ -1,12 +1,24 @@
1
1
  import { type ConfigError } from './errors';
2
2
  import type { ApiErrorContext, BaseApiUrl, PathParams, QueryParams } from './shared';
3
3
  import type { ApiEndpoint } from './types';
4
- export declare function buildUrl(endpoint: ApiEndpoint, params: PathParams | undefined, reqQuery: QueryParams | undefined, baseApiUrls: Record<string, BaseApiUrl>, defaultBaseApiUrl: string | undefined, url: string | undefined, configError: (msg: string) => ConfigError): {
4
+ export type BuildUrlOptions = {
5
+ endpoint: ApiEndpoint;
6
+ params: PathParams | undefined;
7
+ queryParams: QueryParams;
8
+ baseApiUrls: Record<string, BaseApiUrl>;
9
+ defaultBaseApiUrl: string | undefined;
10
+ url: string | undefined;
11
+ doNotEncodeQueryParams?: boolean;
12
+ configError: (msg: string) => ConfigError;
13
+ };
14
+ export declare function buildUrl(options: BuildUrlOptions): {
5
15
  finalUrl: string;
6
16
  fullUrl: string;
7
17
  };
8
- export declare function buildQueryParamsRaw(endpoint: ApiEndpoint, query: QueryParams | undefined): Record<string, string>;
9
- export declare function buildQueryParams(endpoint: ApiEndpoint, query: QueryParams | undefined, ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext): Record<string, string>;
18
+ export declare function buildPathParams(endpoint: ApiEndpoint, params: PathParams | undefined, ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext): PathParams | undefined;
19
+ export declare function buildQueryParams(endpoint: ApiEndpoint, query: QueryParams | undefined, ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext): QueryParams;
10
20
  export declare function buildRequestBody(endpoint: ApiEndpoint, body: unknown, ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext): unknown;
11
21
  export declare function buildHeaders(endpoint: ApiEndpoint, headers: Record<string, string> | undefined, defaultHeaders: Record<string, string>, ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext): Record<string, string>;
22
+ /** Joins with exactly one `/`; empty paths and query-only paths are appended as-is. */
23
+ export declare function joinUrlPath(base: string, path: string): string;
12
24
  export declare function replacePathParams(path: string, params: PathParams | undefined, configError: (msg: string) => ConfigError): string;
@@ -1,6 +1,6 @@
1
1
  import type { ApiAction, ApiClient, ApiClientConfigNoActions, ApiClientConfigWithActions, ApiEndpoint } from './types';
2
2
  export interface CreateApiClientFn {
3
- <T extends Record<string, ApiEndpoint>>(apiConfig: ApiClientConfigNoActions<T>): ApiClient<T, Record<string, never>>;
4
3
  <T extends Record<string, ApiEndpoint>, A extends Record<string, ApiAction>>(apiConfig: ApiClientConfigWithActions<T, A>): ApiClient<T, A>;
4
+ <T extends Record<string, ApiEndpoint>>(apiConfig: ApiClientConfigNoActions<T>): ApiClient<T, Record<string, never>>;
5
5
  }
6
6
  export declare const createApiClient: CreateApiClientFn;
@@ -0,0 +1,16 @@
1
+ import { type ApiError } from './errors';
2
+ /**
3
+ * A multi-line, human-readable summary of an `ApiError` that says up front
4
+ * whether the server, the caller's input, or the response shape is at fault.
5
+ * The client logs this via `logger.error` for every failed request.
6
+ *
7
+ * @example
8
+ * ✗ github.usersGet failed: output validation failed: the response doesn't match resSchema
9
+ * GET https://api.github.com/users/1 · attempt 1/4 · 212ms
10
+ * [1] id: Expected number, received string
11
+ * value: "oops"
12
+ * fix: update resSchema (or resFormatter), or mark changed fields optional
13
+ */
14
+ export declare function describeApiError(error: ApiError): string;
15
+ /** Flat, primitive attributes for structured loggers and log processors. */
16
+ export declare function apiErrorLogAttributes(error: ApiError): Record<string, string | number | boolean>;
@@ -0,0 +1,18 @@
1
+ import type { z } from 'zod';
2
+ import type { ApiEndpoint } from './types';
3
+ /**
4
+ * Defines an endpoint with full inference, as an alternative to
5
+ * `satisfies ApiEndpoint`: `resFormatter` must return what `resSchema`
6
+ * accepts, and a literal `path` types its `:params` (making `reqParams`
7
+ * required when there's no `reqParamsSchema`).
8
+ *
9
+ * @example
10
+ * export const usersGet = defineEndpoint({
11
+ * method: 'GET',
12
+ * path: '/users/:userId',
13
+ * resSchema: User
14
+ * })
15
+ */
16
+ export declare function defineEndpoint<const TEndpoint extends ApiEndpoint>(endpoint: TEndpoint & {
17
+ resFormatter?: (data: unknown, headers: Record<string, string>) => z.input<TEndpoint['resSchema']>;
18
+ }): TEndpoint;
@@ -1,9 +1,22 @@
1
1
  import type { ZodError } from 'zod';
2
2
  import type { ApiErrorContext } from './shared';
3
3
  import { type FormattedZodIssue } from './zod-issues';
4
+ /**
5
+ * Where a request failed, for logs and alerts:
6
+ * - `server_error`: the API returned 5xx
7
+ * - `client_error`: the API rejected the request (4xx: auth, params, body)
8
+ * - `input_validation`: the caller's data failed a request schema; nothing was sent
9
+ * - `output_validation`: the response wasn't JSON or didn't match `resSchema`
10
+ * - `response_rejected`: `checkResponse` rejected a valid response
11
+ * - `network` / `timeout` / `aborted`: the request didn't complete
12
+ * - `config`: the client or call is misconfigured
13
+ * - `action`: a composite action handler threw
14
+ */
15
+ export type ApiErrorCategory = 'server_error' | 'client_error' | 'input_validation' | 'output_validation' | 'response_rejected' | 'network' | 'timeout' | 'aborted' | 'config' | 'action' | 'unknown';
4
16
  export declare class ApiError extends Error {
5
17
  context: ApiErrorContext;
6
18
  cause?: unknown;
19
+ get category(): ApiErrorCategory;
7
20
  constructor(message: string, context: ApiErrorContext, options?: {
8
21
  cause?: unknown;
9
22
  });
@@ -16,6 +29,7 @@ export declare class HttpResponseError extends ApiError {
16
29
  readonly responseHeaders: Record<string, string>;
17
30
  readonly requestId?: string;
18
31
  constructor(status: number, body: string, responseHeaders: Record<string, string>, context: ApiErrorContext);
32
+ get category(): ApiErrorCategory;
19
33
  toJSON(): Record<string, unknown>;
20
34
  }
21
35
  export declare class ResponseValidationError extends ApiError {
@@ -23,10 +37,16 @@ export declare class ResponseValidationError extends ApiError {
23
37
  readonly rawResponse: unknown;
24
38
  readonly issues: FormattedZodIssue[];
25
39
  readonly validationIssues: string;
26
- constructor(zodError: ZodError, rawResponse: unknown, context: ApiErrorContext);
40
+ /** True when the body wasn't valid JSON (rather than failing `resSchema`). */
41
+ readonly invalidJson: boolean;
42
+ constructor(zodError: ZodError, rawResponse: unknown, context: ApiErrorContext, options?: {
43
+ invalidJson?: boolean;
44
+ });
45
+ get category(): ApiErrorCategory;
27
46
  toJSON(): Record<string, unknown>;
28
47
  }
29
- export type RequestValidationTarget = 'body' | 'query' | 'params' | 'headers';
48
+ /** Which request schema failed; `input` is an action's input schema. */
49
+ export type RequestValidationTarget = 'body' | 'query' | 'params' | 'headers' | 'input';
30
50
  export declare class RequestValidationError extends ApiError {
31
51
  readonly zodError: ZodError;
32
52
  readonly target: RequestValidationTarget;
@@ -34,23 +54,39 @@ export declare class RequestValidationError extends ApiError {
34
54
  readonly issues: FormattedZodIssue[];
35
55
  readonly validationIssues: string;
36
56
  constructor(zodError: ZodError, target: RequestValidationTarget, rawInput: unknown, context: ApiErrorContext);
57
+ get category(): ApiErrorCategory;
37
58
  toJSON(): Record<string, unknown>;
38
59
  }
39
60
  export declare class RequestTimeoutError extends ApiError {
40
61
  readonly timeoutMs: number;
41
62
  constructor(timeoutMs: number, context: ApiErrorContext);
63
+ get category(): ApiErrorCategory;
42
64
  }
43
65
  export declare class NetworkError extends ApiError {
44
66
  constructor(message: string, context: ApiErrorContext, cause?: unknown);
67
+ get category(): ApiErrorCategory;
45
68
  }
46
69
  export declare class RequestAbortedError extends ApiError {
47
70
  constructor(context: ApiErrorContext, cause?: unknown);
71
+ get category(): ApiErrorCategory;
48
72
  }
49
73
  export declare class ConfigError extends ApiError {
50
74
  constructor(message: string, context: ApiErrorContext);
75
+ get category(): ApiErrorCategory;
76
+ }
77
+ /**
78
+ * Thrown when the client's `checkResponse` rejects a response that passed
79
+ * schema validation (e.g. HTTP 200 with `{ success: false }`).
80
+ */
81
+ export declare class ApiResponseError extends ApiError {
82
+ readonly data: unknown;
83
+ constructor(message: string, data: unknown, context: ApiErrorContext, cause?: unknown);
84
+ get category(): ApiErrorCategory;
85
+ toJSON(): Record<string, unknown>;
51
86
  }
52
87
  export declare class ActionError extends ApiError {
53
88
  constructor(message: string, context: ApiErrorContext, cause?: unknown);
89
+ get category(): ApiErrorCategory;
54
90
  }
55
91
  export declare const isApiError: (e: unknown) => e is ApiError;
56
92
  export declare const isHttpResponseError: (e: unknown) => e is HttpResponseError;
@@ -61,3 +97,5 @@ export declare const isNetworkError: (e: unknown) => e is NetworkError;
61
97
  export declare const isRequestAbortedError: (e: unknown) => e is RequestAbortedError;
62
98
  export declare const isConfigError: (e: unknown) => e is ConfigError;
63
99
  export declare const isActionError: (e: unknown) => e is ActionError;
100
+ export declare const isApiResponseError: (e: unknown) => e is ApiResponseError;
101
+ export declare function isNonRetryableApiError(error: unknown): boolean;
@@ -1,3 +1,4 @@
1
+ import type { ConfigError } from './errors';
1
2
  /**
2
3
  * Normalize headers to canonical case so lookups are deterministic. Later
3
4
  * occurrences of the same header (case-insensitive) win.
@@ -5,5 +6,5 @@
5
6
  export declare function normalizeHeaders(headers: Record<string, string>): Record<string, string>;
6
7
  export declare function getHeader(headers: Record<string, string>, name: string): string | undefined;
7
8
  export declare function hasJsonContentType(headers: Record<string, string>): boolean;
8
- export declare function buildFetchBody(body: unknown, headers: Record<string, string>): RequestInit['body'];
9
+ export declare function buildFetchBody(body: unknown, headers: Record<string, string>, configError?: (msg: string) => ConfigError): RequestInit['body'];
9
10
  export declare function getHeadersAsObject(headers: Headers): Record<string, string>;
@@ -3,3 +3,8 @@ export * from './types';
3
3
  export * from './errors';
4
4
  export { createApiClient } from './create-client';
5
5
  export { defineAction } from './actions-impl';
6
+ export { type PaginateOptions, paginate } from './paginate';
7
+ export { defaultShouldRetry } from './transport';
8
+ export { apiErrorLogAttributes, describeApiError } from './describe';
9
+ export * from './otel';
10
+ export { defineEndpoint } from './endpoint';
@@ -0,0 +1,44 @@
1
+ import type { ApiMiddleware, ApiMiddlewareContext } from './types';
2
+ export type OtelAttributes = Record<string, string | number | boolean | undefined>;
3
+ export type OtelSpanLike = {
4
+ setAttributes(attributes: OtelAttributes): unknown;
5
+ setStatus(status: {
6
+ code: number;
7
+ message?: string;
8
+ }): unknown;
9
+ recordException(exception: Error | string): unknown;
10
+ end(): void;
11
+ };
12
+ export type OtelTracerLike = {
13
+ startActiveSpan<F extends (span: OtelSpanLike) => unknown>(name: string, options: {
14
+ kind?: number;
15
+ attributes?: OtelAttributes;
16
+ }, fn: F): ReturnType<F>;
17
+ };
18
+ export type OtelMiddlewareOptions = {
19
+ /** Default: `client.endpoint`, or the endpoint key when the client has no `name`. */
20
+ spanName?: (context: ApiMiddlewareContext) => string;
21
+ /** Extra attributes set when the span starts. */
22
+ attributes?: (context: ApiMiddlewareContext) => OtelAttributes;
23
+ /**
24
+ * `url.full` keeps the query string with sensitive values redacted
25
+ * (default). Set `false` to drop the query string entirely.
26
+ */
27
+ includeQuery?: boolean;
28
+ /**
29
+ * Adds trace-context headers to the outgoing request, e.g.
30
+ * `(headers) => propagation.inject(context.active(), headers)`. Runs inside
31
+ * the active span. Not needed when fetch is already auto-instrumented.
32
+ */
33
+ inject?: (headers: Record<string, string>) => void;
34
+ };
35
+ /**
36
+ * One CLIENT span per logical request (all retries, validation and
37
+ * `checkResponse`), with OpenTelemetry HTTP semantic-convention attributes.
38
+ * It's the active span, so auto-instrumented fetch attempts become children.
39
+ *
40
+ * @example
41
+ * import { trace } from '@opentelemetry/api'
42
+ * createApiClient({ ...config, use: [otelMiddleware(trace.getTracer('app'))] })
43
+ */
44
+ export declare function otelMiddleware(tracer: OtelTracerLike, options?: OtelMiddlewareOptions): ApiMiddleware;
@@ -0,0 +1,12 @@
1
+ export type PaginateOptions = {
2
+ /** Stop after this many pages (default: no limit). */
3
+ maxPages?: number;
4
+ /** Checked before each page fetch; throws `signal.reason` when aborted. */
5
+ signal?: AbortSignal;
6
+ };
7
+ /**
8
+ * Yields pages until `getNext` returns `null`/`undefined`. The first call to
9
+ * `fetchPage` gets `undefined` as cursor. Pages are fetched lazily, so
10
+ * breaking out of `for await` stops further requests.
11
+ */
12
+ export declare function paginate<TPage, TCursor>(fetchPage: (cursor: TCursor | undefined) => Promise<TPage>, getNext: (page: TPage) => TCursor | null | undefined, options?: PaginateOptions): AsyncGenerator<TPage, void, undefined>;
@@ -1,2 +1,4 @@
1
1
  import type { ApiErrorContext } from './shared';
2
+ /** Replaces values of sensitive query parameters (`?api_key=…`) in a URL. */
3
+ export declare function redactUrl(url: string): string;
2
4
  export declare function defaultRedactContext(ctx: ApiErrorContext): ApiErrorContext;
@@ -1,8 +1,22 @@
1
+ import type { UrlQueryParams } from '../url';
1
2
  export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
2
3
  export type RequestBodyFormat = 'json' | 'form-data';
3
4
  export type BaseApiUrl = string | (() => string);
4
5
  export type PathParams = Record<string, string | number>;
5
- export type QueryParams = Record<string, string | number | boolean | undefined>;
6
+ /** Array values repeat the key (`a=1&a=2`); `null`/`undefined` are skipped. */
7
+ export type QueryParams = UrlQueryParams;
8
+ /**
9
+ * Augment to type `meta` on requests, actions, hooks, middleware and errors:
10
+ *
11
+ * @example
12
+ * declare module 'hanzio/api-wrapper' {
13
+ * interface ApiRequestMeta { traceId: string; tenantId?: string }
14
+ * }
15
+ */
16
+ export interface ApiRequestMeta {
17
+ }
18
+ /** `ApiRequestMeta` once augmented, otherwise any record. */
19
+ export type RequestMeta = keyof ApiRequestMeta extends never ? Record<string, unknown> : ApiRequestMeta;
6
20
  export type ApiErrorContext = {
7
21
  endpoint: string;
8
22
  method: HttpMethod;
@@ -12,5 +26,5 @@ export type ApiErrorContext = {
12
26
  elapsedMs: number;
13
27
  requestHeaders?: Record<string, string>;
14
28
  requestBody?: unknown;
15
- meta?: Record<string, unknown>;
29
+ meta?: RequestMeta;
16
30
  };
@@ -0,0 +1,23 @@
1
+ import type { FetchLike } from './types';
2
+ export type MockFetch = FetchLike & {
3
+ /** Every request received, in order. Bodies are still readable. */
4
+ readonly calls: Request[];
5
+ };
6
+ /**
7
+ * A fetch stand-in for tests: pass it as the client's `fetch` option instead
8
+ * of replacing `globalThis.fetch`. Test-runner agnostic.
9
+ *
10
+ * @example
11
+ * const fetch = createMockFetch((request) =>
12
+ * request.url.endsWith('/users/1')
13
+ * ? jsonResponse({ id: 1 })
14
+ * : jsonResponse({ error: 'not found' }, { status: 404 })
15
+ * )
16
+ * const api = createApiClient({ ...config, fetch })
17
+ */
18
+ export declare function createMockFetch(handler: (request: Request, callIndex: number) => Response | Promise<Response>): MockFetch;
19
+ /** A JSON `Response` with `content-type: application/json`. */
20
+ export declare function jsonResponse(body: unknown, init?: {
21
+ status?: number;
22
+ headers?: Record<string, string>;
23
+ }): Response;
@@ -1,20 +1,17 @@
1
1
  import { type z } from 'zod';
2
- import type { ApiEndpoint, OnResponseContext, OnRetryContext, RetryContext } from './types';
3
- import type { ApiErrorContext, HttpMethod } from './shared';
2
+ import type { ApiEndpoint, FetchLike, OnResponseContext, OnRetryContext, RetryContext } from './types';
3
+ import type { ApiErrorContext, HttpMethod, RequestMeta } from './shared';
4
4
  export type RequestConfig = {
5
5
  method: HttpMethod;
6
- url: string;
7
- queryParams: Record<string, string>;
6
+ fullUrl: string;
8
7
  body: unknown;
9
8
  headers: Record<string, string>;
10
- doNotEncodeQueryParams?: boolean;
11
9
  timeoutMs?: number;
12
- fetchFn: typeof fetch;
10
+ fetchFn: FetchLike;
13
11
  userSignal?: AbortSignal;
14
12
  };
15
13
  export declare function validateResponse<T extends z.ZodType>(schema: T, data: unknown, ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext): Promise<z.infer<T>>;
16
14
  export declare function httpRequest(config: RequestConfig, ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext): Promise<Response>;
17
- export declare function mergeSignals(a: AbortSignal, b: AbortSignal | undefined): AbortSignal;
18
15
  export declare function readErrorResponse(response: Response): Promise<{
19
16
  body: string;
20
17
  headers: Record<string, string>;
@@ -24,6 +21,7 @@ export declare function getRetryDelay(params: {
24
21
  retryDelay: number | ((attempt: number) => number);
25
22
  attempt: number;
26
23
  response?: Response;
24
+ maxRetryDelayMs?: number;
27
25
  }): number;
28
26
  export declare function shouldAttemptRetryOnError(error: unknown): boolean;
29
27
  export declare function shouldAttemptRetryOnHttp(retryCtx: RetryContext): boolean;
@@ -31,14 +29,15 @@ type MakeRequestArgs<TValidated> = {
31
29
  config: RequestConfig;
32
30
  maxRetries: number;
33
31
  retryDelay: number | ((attempt: number) => number);
34
- validateFn: (response: Response) => Promise<TValidated>;
32
+ maxRetryDelayMs?: number;
33
+ validateFn: (response: Response, attempt: number) => Promise<TValidated>;
35
34
  shouldRetry: (context: RetryContext) => boolean;
36
35
  logger?: Pick<Console, 'debug'>;
37
36
  ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext;
38
37
  endpointName: string;
39
38
  method: HttpMethod;
40
39
  fullUrl: string;
41
- meta?: Record<string, unknown>;
40
+ meta?: RequestMeta;
42
41
  onResponse?: (context: OnResponseContext) => void | Promise<void>;
43
42
  onRetry?: (context: OnRetryContext) => void | Promise<void>;
44
43
  };
@@ -47,6 +46,9 @@ export declare function makeRequestWithRetry<TValidated>(args: MakeRequestArgs<T
47
46
  retryCount: number;
48
47
  httpStatus: number;
49
48
  }>;
50
- export declare function validateAndTransformResponse<T extends z.ZodType>(endpoint: ApiEndpoint<z.ZodType | undefined, z.ZodType | undefined, z.ZodType | undefined, z.ZodType | undefined, T>, response: Response, ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext): Promise<z.infer<T>>;
49
+ export declare function validateAndTransformResponse<T extends z.ZodType>(endpoint: ApiEndpoint<z.ZodType | undefined, z.ZodType | undefined, z.ZodType | undefined, z.ZodType | undefined, T>, response: Response, ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext): Promise<{
50
+ data: z.infer<T>;
51
+ responseSizeBytes: number;
52
+ }>;
51
53
  export declare function defaultShouldRetry(ctx: RetryContext): boolean;
52
54
  export {};
@@ -1,6 +1,7 @@
1
1
  import type { z } from 'zod';
2
+ import type { Result } from '../promise';
2
3
  import type { ApiError } from './errors';
3
- import type { ApiErrorContext, BaseApiUrl, HttpMethod, PathParams, QueryParams, RequestBodyFormat } from './shared';
4
+ import type { ApiErrorContext, BaseApiUrl, HttpMethod, PathParams, QueryParams, RequestBodyFormat, RequestMeta } from './shared';
4
5
  export interface ApiEndpoint<TReqBody extends z.ZodType | undefined = z.ZodType | undefined, TReqParams extends z.ZodType | undefined = z.ZodType | undefined, TReqQuery extends z.ZodType | undefined = z.ZodType | undefined, TReqHeaders extends z.ZodType | undefined = z.ZodType | undefined, TResponse extends z.ZodType = z.ZodType> {
5
6
  method: HttpMethod;
6
7
  path: string | ((baseUrl: string) => string);
@@ -12,7 +13,8 @@ export interface ApiEndpoint<TReqBody extends z.ZodType | undefined = z.ZodType
12
13
  baseApiUrl?: string;
13
14
  reqBodyFormat?: RequestBodyFormat;
14
15
  defaultHeaders?: Record<string, string>;
15
- resFormatter?: (data: unknown, headers: Record<string, string>) => z.infer<TResponse>;
16
+ /** Reshapes the raw body before `resSchema` parses it, so it returns the schema's input type. */
17
+ resFormatter?: (data: unknown, headers: Record<string, string>) => z.input<TResponse>;
16
18
  reqDefaultQueryParams?: QueryParams;
17
19
  doNotEncodeQueryParams?: boolean;
18
20
  }
@@ -47,7 +49,7 @@ export type OnRequestContext = {
47
49
  url: string;
48
50
  headers: Record<string, string>;
49
51
  body: unknown;
50
- meta?: Record<string, unknown>;
52
+ meta?: RequestMeta;
51
53
  };
52
54
  export type OnResponseContext = {
53
55
  endpoint: string;
@@ -55,7 +57,7 @@ export type OnResponseContext = {
55
57
  url: string;
56
58
  response: Response;
57
59
  attempt: number;
58
- meta?: Record<string, unknown>;
60
+ meta?: RequestMeta;
59
61
  };
60
62
  export type ActionCache = {
61
63
  <T>(key: string, fn: () => Promise<T> | T, options?: {
@@ -70,75 +72,159 @@ export type ActionCache = {
70
72
  };
71
73
  export type ActionInvokeOptions = {
72
74
  signal?: AbortSignal;
73
- meta?: Record<string, unknown>;
75
+ meta?: RequestMeta;
74
76
  };
75
77
  export type ActionContext<TInput, TApi = any> = {
76
78
  input: TInput;
77
79
  api: TApi;
78
80
  signal?: AbortSignal;
79
- meta?: Record<string, unknown>;
81
+ meta?: RequestMeta;
80
82
  logger?: Pick<Console, 'debug' | 'error'>;
81
83
  cache: ActionCache;
82
84
  };
83
85
  export interface ApiAction<TInput = any, TOutput = any> {
84
86
  handler: (ctx: ActionContext<TInput>) => Promise<TOutput> | TOutput;
87
+ /** Validates the caller's input at runtime; `handler` receives the parsed output. */
88
+ readonly input?: z.ZodType;
85
89
  /** When true, the action accepts only optional `{ signal, meta }`; input is always `undefined`. */
86
90
  readonly noRuntimeInput?: boolean;
87
91
  }
88
92
  type ActionDefinition<TInput, TOutput, TApi> = {
89
93
  handler: (ctx: ActionContext<TInput, TApi>) => Promise<TOutput> | TOutput;
90
94
  };
95
+ type SchemaActionDefinition<TSchema extends z.ZodType, TOutput, TApi> = {
96
+ input: TSchema;
97
+ handler: (ctx: ActionContext<z.output<TSchema>, TApi>) => Promise<TOutput> | TOutput;
98
+ };
91
99
  export type DefineAction<TApi> = {
100
+ /** Input validated by a Zod schema: callers pass `z.input`, the handler gets `z.output`. */
101
+ <TSchema extends z.ZodType, TOutput>(def: SchemaActionDefinition<TSchema, TOutput, TApi>): ApiAction<z.input<TSchema>, TOutput>;
92
102
  <TOutput>(def: ActionDefinition<undefined, TOutput, TApi>): ApiAction<undefined, TOutput>;
93
103
  <TInput>(): <TOutput>(def: ActionDefinition<TInput, TOutput, TApi>) => ApiAction<TInput, TOutput>;
94
104
  };
95
105
  type ActionInput<A> = A extends ApiAction<infer I, any> ? I : never;
96
106
  export type ActionOutput<A> = A extends ApiAction<any, infer O> ? O : never;
97
107
  export type ActionArgs<A> = [ActionInput<A>] extends [undefined] ? [options?: ActionInvokeOptions] : [input: ActionInput<A>, options?: ActionInvokeOptions];
98
- type SchemaInputKeys<TEndpoint extends ApiEndpoint> = (TEndpoint['reqBodySchema'] extends z.ZodType ? 'reqBody' : never) | (TEndpoint['reqParamsSchema'] extends z.ZodType ? 'reqParams' : never) | (TEndpoint['reqQuerySchema'] extends z.ZodType ? 'reqQuery' : never) | (TEndpoint['reqHeadersSchema'] extends z.ZodType ? 'reqHeaders' : never);
108
+ type CharsOf<S extends string, Acc extends string = never> = S extends `${infer C}${infer Rest}` ? CharsOf<Rest, Acc | C> : Acc;
109
+ type IdentStart = CharsOf<'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_'>;
110
+ type WordChar = IdentStart | CharsOf<'0123456789'>;
111
+ type TakeWord<S extends string, Acc extends string = ''> = S extends `${infer C}${infer Rest}` ? C extends WordChar ? TakeWord<Rest, `${Acc}${C}`> : Acc : Acc;
112
+ /**
113
+ * `:name` placeholders in a literal path, matching the runtime rule (a letter
114
+ * or `_`, then word characters, so ports like `:8080` don't count).
115
+ * `never` for non-literal paths.
116
+ */
117
+ export type PathParamNames<TPath> = TPath extends string ? string extends TPath ? never : TPath extends `${string}:${infer Rest}` ? (Rest extends `${IdentStart}${string}` ? TakeWord<Rest> : never) | PathParamNames<Rest> : never : never;
118
+ type EndpointPathParamNames<TEndpoint extends ApiEndpoint> = PathParamNames<TEndpoint['path']>;
119
+ type SchemaInputKeys<TEndpoint extends ApiEndpoint> = (TEndpoint['reqBodySchema'] extends z.ZodType ? 'reqBody' : never) | (TEndpoint['reqQuerySchema'] extends z.ZodType ? 'reqQuery' : never) | (TEndpoint['reqHeadersSchema'] extends z.ZodType ? 'reqHeaders' : never) | (TEndpoint['reqParamsSchema'] extends z.ZodType ? 'reqParams' : [EndpointPathParamNames<TEndpoint>] extends [never] ? never : 'reqParams');
99
120
  type SchemaInputs<TEndpoint extends ApiEndpoint> = {
100
- reqBody: TEndpoint['reqBodySchema'] extends z.ZodType ? z.infer<TEndpoint['reqBodySchema']> : unknown;
101
- reqParams: TEndpoint['reqParamsSchema'] extends z.ZodType ? z.infer<TEndpoint['reqParamsSchema']> : PathParams;
102
- reqQuery: TEndpoint['reqQuerySchema'] extends z.ZodType ? z.infer<TEndpoint['reqQuerySchema']> : QueryParams;
103
- reqHeaders: TEndpoint['reqHeadersSchema'] extends z.ZodType ? z.infer<TEndpoint['reqHeadersSchema']> : Record<string, string>;
121
+ reqBody: TEndpoint['reqBodySchema'] extends z.ZodType ? z.input<TEndpoint['reqBodySchema']> : unknown;
122
+ reqParams: TEndpoint['reqParamsSchema'] extends z.ZodType ? z.input<TEndpoint['reqParamsSchema']> : [EndpointPathParamNames<TEndpoint>] extends [never] ? PathParams : Record<EndpointPathParamNames<TEndpoint>, string | number>;
123
+ reqQuery: TEndpoint['reqQuerySchema'] extends z.ZodType ? z.input<TEndpoint['reqQuerySchema']> : QueryParams;
124
+ reqHeaders: TEndpoint['reqHeadersSchema'] extends z.ZodType ? z.input<TEndpoint['reqHeadersSchema']> : Record<string, string>;
104
125
  };
105
126
  type CommonRequestOptions = {
106
127
  url?: string;
107
128
  signal?: AbortSignal;
108
129
  timeoutMs?: number;
109
130
  retries?: number;
110
- fetch?: typeof fetch;
111
- meta?: Record<string, unknown>;
131
+ fetch?: FetchLike;
132
+ meta?: RequestMeta;
112
133
  };
113
134
  export type RequestInput<TEndpoint extends ApiEndpoint> = Pick<SchemaInputs<TEndpoint>, SchemaInputKeys<TEndpoint>> & Partial<Omit<SchemaInputs<TEndpoint>, SchemaInputKeys<TEndpoint>>> & CommonRequestOptions;
114
135
  type RequestArgs<TEndpoint extends ApiEndpoint> = SchemaInputKeys<TEndpoint> extends never ? [input?: RequestInput<TEndpoint>] : [input: RequestInput<TEndpoint>];
115
136
  export type ApiClient<T extends Record<string, ApiEndpoint>, A extends Record<string, ApiAction> = Record<string, never>> = {
116
137
  request: {
117
- <K extends keyof T>(endpointKey: K, ...args: RequestArgs<T[K]>): Promise<ApiWrapperResponse<z.infer<T[K]['resSchema']>>>;
138
+ <K extends keyof T>(endpointKey: K, ...args: RequestArgs<T[K]>): Promise<ApiWrapperResponse<EndpointResponse<T[K]>>>;
118
139
  <K extends keyof A>(actionKey: K, ...args: ActionArgs<A[K]>): Promise<ActionOutput<A[K]>>;
119
140
  };
120
141
  cache: ActionCache;
142
+ /**
143
+ * The same endpoints and actions, resolving to `Result` instead of throwing
144
+ * `ApiError`s. Other errors (bugs in hooks or middleware) still throw.
145
+ */
146
+ safe: {
147
+ [K in keyof T]: (...args: RequestArgs<T[K]>) => Promise<Result<ApiWrapperResponse<EndpointResponse<T[K]>>, ApiError>>;
148
+ } & {
149
+ [K in keyof A]: (...args: ActionArgs<A[K]>) => Promise<Result<ActionOutput<A[K]>, ApiError>>;
150
+ };
121
151
  } & {
122
- [K in keyof T]: (...args: RequestArgs<T[K]>) => Promise<ApiWrapperResponse<z.infer<T[K]['resSchema']>>>;
152
+ [K in keyof T]: (...args: RequestArgs<T[K]>) => Promise<ApiWrapperResponse<EndpointResponse<T[K]>>>;
123
153
  } & {
124
154
  [K in keyof A]: (...args: ActionArgs<A[K]>) => Promise<ActionOutput<A[K]>>;
125
155
  };
156
+ /**
157
+ * Any client, for helpers that accept whichever client they're given. Call
158
+ * endpoints through `request` or `safe`; results are `unknown`.
159
+ */
160
+ export type AnyApiClient = {
161
+ request(key: string, ...args: any[]): Promise<unknown>;
162
+ cache: ActionCache;
163
+ safe: Record<string, (...args: any[]) => Promise<Result<unknown, ApiError>>>;
164
+ };
165
+ /** Parsed response data of an endpoint definition (`z.output` of `resSchema`). */
166
+ export type EndpointResponse<TEndpoint extends ApiEndpoint> = z.output<TEndpoint['resSchema']>;
167
+ /** Request input for an endpoint definition (`reqBody`, `reqParams`, …). */
168
+ export type EndpointRequest<TEndpoint extends ApiEndpoint> = RequestInput<TEndpoint>;
169
+ /** Response data of a client method: `InferResponse<typeof api, 'usersGet'>`. */
170
+ export type InferResponse<TClient, K extends keyof TClient> = TClient[K] extends (...args: never[]) => Promise<infer R> ? R extends ApiWrapperResponse<infer D> ? D : R : never;
171
+ /** First argument of a client method: `InferRequest<typeof api, 'usersGet'>`. */
172
+ export type InferRequest<TClient, K extends keyof TClient> = TClient[K] extends (...args: infer P) => unknown ? NonNullable<P[0]> : never;
126
173
  export type ActionsFactoryHelpers<T extends Record<string, ApiEndpoint>, A extends Record<string, ApiAction> = Record<string, never>> = {
127
174
  api: ApiClient<T, A>;
128
175
  defineAction: DefineAction<ApiClient<T, A>>;
129
176
  };
177
+ /** Any fetch-compatible function; avoids Bun's `typeof fetch` extras like `preconnect`. */
178
+ export type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
179
+ export type ApiMiddlewareContext = {
180
+ /** The client's `name`, when set. */
181
+ client?: string;
182
+ /** The endpoint key, e.g. `usersGet`. */
183
+ endpoint: string;
184
+ method: HttpMethod;
185
+ /** Full URL including the query string. */
186
+ url: string;
187
+ /** Outgoing headers. Mutate before `next()` to add headers (e.g. `traceparent`). */
188
+ headers: Record<string, string>;
189
+ meta?: RequestMeta;
190
+ };
191
+ /**
192
+ * Wraps one logical request: `onRequest`, every retry attempt, validation and
193
+ * `checkResponse`. Call `next()` to continue; errors it throws are `ApiError`s
194
+ * that were already redacted and passed to `onError`.
195
+ */
196
+ export type ApiMiddleware = (context: ApiMiddlewareContext, next: () => Promise<ApiWrapperResponse<unknown>>) => Promise<ApiWrapperResponse<unknown>>;
197
+ export type CheckResponseContext = {
198
+ endpoint: string;
199
+ method: HttpMethod;
200
+ url: string;
201
+ httpStatus: number;
202
+ attempt: number;
203
+ meta?: RequestMeta;
204
+ };
205
+ export type DefaultHeaders = Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);
130
206
  export interface ApiClientConfig<T extends Record<string, ApiEndpoint>, A extends Record<string, ApiAction> = Record<string, never>> {
131
207
  name?: string;
132
208
  baseApiUrls: Record<string, BaseApiUrl>;
133
209
  defaultBaseApiUrl?: string;
134
210
  endpoints: T;
135
- defaultHeaders?: Record<string, string>;
211
+ /** Static headers, or a function resolved on every request (e.g. auth from a secret store). */
212
+ defaultHeaders?: DefaultHeaders;
136
213
  timeoutMs?: number;
137
214
  retries?: number;
138
215
  retryDelayMs?: number | ((attempt: number) => number);
216
+ maxRetryDelayMs?: number;
139
217
  shouldRetry?: (context: RetryContext) => boolean;
140
218
  logger?: Pick<Console, 'debug' | 'error'>;
141
- fetch?: typeof fetch;
219
+ fetch?: FetchLike;
220
+ /** Middleware around each whole request, in array order (first is outermost). */
221
+ use?: ApiMiddleware[];
222
+ /**
223
+ * Runs after `resSchema` validation on every attempt. Throw to reject the
224
+ * response; non-`ApiError` throws become `ApiResponseError`, which the
225
+ * default retry policy does not retry.
226
+ */
227
+ checkResponse?: (data: unknown, context: CheckResponseContext) => void | Promise<void>;
142
228
  onRequest?: (context: OnRequestContext) => RequestInterceptorResult | Promise<RequestInterceptorResult>;
143
229
  onResponse?: (context: OnResponseContext) => void | Promise<void>;
144
230
  onError?: (error: ApiError) => void | Promise<void>;
@@ -150,4 +236,4 @@ export type ApiClientConfigNoActions<T extends Record<string, ApiEndpoint>> = Om
150
236
  export type ApiClientConfigWithActions<T extends Record<string, ApiEndpoint>, A extends Record<string, ApiAction>> = Omit<ApiClientConfig<T, Record<string, never>>, 'actions'> & {
151
237
  actions: A | ((helpers: ActionsFactoryHelpers<T, A>) => A);
152
238
  };
153
- export type { BaseApiUrl, HttpMethod, PathParams, QueryParams, RequestBodyFormat, ApiErrorContext } from './shared';
239
+ export type { ApiRequestMeta, RequestMeta, BaseApiUrl, HttpMethod, PathParams, QueryParams, RequestBodyFormat, ApiErrorContext } from './shared';