hanzio 1.0.10 → 1.2.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 (71) hide show
  1. package/README.md +70 -4
  2. package/dist/api-wrapper/actions-impl.d.ts +4 -0
  3. package/dist/api-wrapper/builders.d.ts +23 -0
  4. package/dist/api-wrapper/create-client.d.ts +6 -0
  5. package/dist/api-wrapper/error-reporting.d.ts +7 -0
  6. package/dist/api-wrapper/errors.d.ts +64 -0
  7. package/dist/api-wrapper/headers.d.ts +10 -0
  8. package/dist/api-wrapper/index.d.ts +5 -84
  9. package/dist/api-wrapper/redaction.d.ts +2 -0
  10. package/dist/api-wrapper/shared.d.ts +16 -0
  11. package/dist/api-wrapper/size.d.ts +1 -0
  12. package/dist/api-wrapper/test-helpers.d.ts +6 -0
  13. package/dist/api-wrapper/transport.d.ts +55 -0
  14. package/dist/api-wrapper/types.d.ts +154 -0
  15. package/dist/api-wrapper/zod-issues.d.ts +17 -0
  16. package/dist/cache/index.d.ts +26 -0
  17. package/dist/secrets/core.d.ts +47 -0
  18. package/dist/secrets/deadline.d.ts +6 -0
  19. package/dist/secrets/environment.d.ts +9 -0
  20. package/dist/secrets/errors.d.ts +11 -0
  21. package/dist/secrets/index.d.ts +14 -23
  22. package/dist/secrets/infisical-cli.d.ts +6 -0
  23. package/dist/secrets/infisical-config.d.ts +17 -0
  24. package/dist/secrets/infisical-session.d.ts +4 -0
  25. package/dist/secrets/infisical.d.ts +14 -2
  26. package/dist/secrets/loaders.d.ts +9 -0
  27. package/dist/secrets/vite.d.ts +7 -3
  28. package/dist/secrets/worker-loader.d.ts +9 -0
  29. package/dist/secrets/worker.d.ts +3 -0
  30. package/dist/src/api-wrapper/index.js +1059 -177
  31. package/dist/src/index.js +2807 -1397
  32. package/dist/src/jwt/index.js +2 -2
  33. package/dist/src/p-queue/index.js +3 -3
  34. package/dist/src/secrets/index.js +1610 -407
  35. package/dist/src/secrets/infisical.js +1352 -214
  36. package/dist/src/secrets/loaders.js +1529 -0
  37. package/dist/src/secrets/vite.js +33 -11
  38. package/dist/src/secrets/worker.js +275 -0
  39. package/dist/src/sitemap/index.js +1 -1
  40. package/dist/src/zod/index.js +7 -7
  41. package/dist/typedswitch/index.d.ts +9 -5
  42. package/package.json +12 -3
  43. package/src/api-wrapper/actions-impl.ts +100 -0
  44. package/src/api-wrapper/builders.ts +200 -0
  45. package/src/api-wrapper/create-client.ts +393 -0
  46. package/src/api-wrapper/error-reporting.ts +31 -0
  47. package/src/api-wrapper/errors.ts +231 -0
  48. package/src/api-wrapper/headers.ts +79 -0
  49. package/src/api-wrapper/index.ts +5 -492
  50. package/src/api-wrapper/redaction.ts +62 -0
  51. package/src/api-wrapper/shared.ts +25 -0
  52. package/src/api-wrapper/size.ts +19 -0
  53. package/src/api-wrapper/test-helpers.ts +25 -0
  54. package/src/api-wrapper/transport.ts +350 -0
  55. package/src/api-wrapper/types.ts +259 -0
  56. package/src/api-wrapper/zod-issues.ts +157 -0
  57. package/src/cache/index.ts +228 -61
  58. package/src/secrets/core.ts +348 -0
  59. package/src/secrets/deadline.ts +69 -0
  60. package/src/secrets/environment.ts +69 -0
  61. package/src/secrets/errors.ts +29 -0
  62. package/src/secrets/index.ts +81 -140
  63. package/src/secrets/infisical-cli.ts +99 -0
  64. package/src/secrets/infisical-config.ts +70 -0
  65. package/src/secrets/infisical-session.ts +42 -0
  66. package/src/secrets/infisical.ts +219 -35
  67. package/src/secrets/loaders.ts +43 -0
  68. package/src/secrets/vite.ts +44 -15
  69. package/src/secrets/worker-loader.ts +56 -0
  70. package/src/secrets/worker.ts +4 -0
  71. package/src/typedswitch/index.ts +80 -69
@@ -0,0 +1,350 @@
1
+ import { ZodError, type z } from 'zod'
2
+ import type {
3
+ ApiEndpoint,
4
+ OnResponseContext,
5
+ OnRetryContext,
6
+ RetryContext
7
+ } from './types'
8
+ import type { ApiErrorContext, HttpMethod } from './shared'
9
+ import {
10
+ ConfigError,
11
+ HttpResponseError,
12
+ isNonRetryableApiError,
13
+ NetworkError,
14
+ RequestAbortedError,
15
+ RequestTimeoutError,
16
+ ResponseValidationError
17
+ } from './errors'
18
+ import {
19
+ buildFetchBody,
20
+ getHeadersAsObject,
21
+ hasJsonContentType
22
+ } from './headers'
23
+ import { makeJsonParseZodError } from './zod-issues'
24
+
25
+ export type RequestConfig = {
26
+ method: HttpMethod
27
+ fullUrl: string
28
+ body: unknown
29
+ headers: Record<string, string>
30
+ timeoutMs?: number
31
+ fetchFn: typeof fetch
32
+ userSignal?: AbortSignal
33
+ }
34
+
35
+ export async function validateResponse<T extends z.ZodType>(
36
+ schema: T,
37
+ data: unknown,
38
+ ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext
39
+ ): Promise<z.infer<T>> {
40
+ try {
41
+ return await schema.parseAsync(data)
42
+ } catch (error) {
43
+ if (error instanceof ZodError) {
44
+ throw new ResponseValidationError(error, data, ctx())
45
+ }
46
+ throw error
47
+ }
48
+ }
49
+
50
+ export async function httpRequest(
51
+ config: RequestConfig,
52
+ ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext
53
+ ): Promise<Response> {
54
+ const timeoutController = new AbortController()
55
+ const timeout =
56
+ config.timeoutMs === undefined
57
+ ? undefined
58
+ : setTimeout(() => timeoutController.abort(), config.timeoutMs)
59
+
60
+ const signal = mergeSignals(timeoutController.signal, config.userSignal)
61
+
62
+ try {
63
+ return await config.fetchFn(config.fullUrl, {
64
+ method: config.method,
65
+ headers: config.headers,
66
+ body: buildFetchBody(
67
+ config.body,
68
+ config.headers,
69
+ (msg) => new ConfigError(msg, ctx())
70
+ ),
71
+ signal
72
+ })
73
+ } catch (error) {
74
+ if (isDomAbortError(error)) {
75
+ if (config.userSignal?.aborted) {
76
+ throw new RequestAbortedError(ctx(), error)
77
+ }
78
+ if (timeoutController.signal.aborted && config.timeoutMs !== undefined) {
79
+ throw new RequestTimeoutError(config.timeoutMs, ctx())
80
+ }
81
+ throw new RequestAbortedError(ctx(), error)
82
+ }
83
+ if (error instanceof ConfigError) throw error
84
+ throw new NetworkError(
85
+ error instanceof Error ? error.message : String(error),
86
+ ctx(),
87
+ error
88
+ )
89
+ } finally {
90
+ if (timeout) clearTimeout(timeout)
91
+ }
92
+ }
93
+
94
+ function isDomAbortError(error: unknown): boolean {
95
+ return (
96
+ typeof error === 'object' &&
97
+ error !== null &&
98
+ 'name' in error &&
99
+ (error as { name: string }).name === 'AbortError'
100
+ )
101
+ }
102
+
103
+ export function mergeSignals(
104
+ a: AbortSignal,
105
+ b: AbortSignal | undefined
106
+ ): AbortSignal {
107
+ if (!b) return a
108
+ const anyFn = (
109
+ AbortSignal as unknown as {
110
+ any?: (signals: AbortSignal[]) => AbortSignal
111
+ }
112
+ ).any
113
+ if (typeof anyFn === 'function') return anyFn([a, b])
114
+
115
+ const controller = new AbortController()
116
+ const onAbort = () => controller.abort()
117
+ if (a.aborted) controller.abort()
118
+ else a.addEventListener('abort', onAbort, { once: true })
119
+ if (b.aborted) controller.abort()
120
+ else b.addEventListener('abort', onAbort, { once: true })
121
+ return controller.signal
122
+ }
123
+
124
+ export async function readErrorResponse(response: Response): Promise<{
125
+ body: string
126
+ headers: Record<string, string>
127
+ status: number
128
+ }> {
129
+ const body = await response.text()
130
+ return {
131
+ body,
132
+ headers: getHeadersAsObject(response.headers),
133
+ status: response.status
134
+ }
135
+ }
136
+
137
+ export function getRetryDelay(params: {
138
+ retryDelay: number | ((attempt: number) => number)
139
+ attempt: number
140
+ response?: Response
141
+ maxRetryDelayMs?: number
142
+ }): number {
143
+ const maxDelay = params.maxRetryDelayMs ?? 30_000
144
+ const { retryDelay, attempt, response } = params
145
+ if (response && (response.status === 429 || response.status === 503)) {
146
+ const retryAfter = response.headers.get('retry-after')
147
+ if (retryAfter) {
148
+ const seconds = Number(retryAfter)
149
+ if (Number.isFinite(seconds)) {
150
+ return Math.min(Math.max(0, seconds * 1000), maxDelay)
151
+ }
152
+ const dateMs = Date.parse(retryAfter)
153
+ if (!Number.isNaN(dateMs)) {
154
+ return Math.min(Math.max(0, dateMs - Date.now()), maxDelay)
155
+ }
156
+ }
157
+ }
158
+ const delayMs =
159
+ typeof retryDelay === 'function' ? retryDelay(attempt) : retryDelay
160
+ return Math.min(delayMs, maxDelay)
161
+ }
162
+
163
+ export function shouldAttemptRetryOnError(error: unknown): boolean {
164
+ return !isNonRetryableApiError(error)
165
+ }
166
+
167
+ export function shouldAttemptRetryOnHttp(retryCtx: RetryContext): boolean {
168
+ const { error, response } = retryCtx
169
+ if (error) return shouldAttemptRetryOnError(error)
170
+ return response ? response.status >= 500 || response.status === 429 : false
171
+ }
172
+
173
+ type MakeRequestArgs<TValidated> = {
174
+ config: RequestConfig
175
+ maxRetries: number
176
+ retryDelay: number | ((attempt: number) => number)
177
+ maxRetryDelayMs?: number
178
+ validateFn: (response: Response, attempt: number) => Promise<TValidated>
179
+ shouldRetry: (context: RetryContext) => boolean
180
+ logger?: Pick<Console, 'debug'>
181
+ ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext
182
+ endpointName: string
183
+ method: HttpMethod
184
+ fullUrl: string
185
+ meta?: Record<string, unknown>
186
+ onResponse?: (context: OnResponseContext) => void | Promise<void>
187
+ onRetry?: (context: OnRetryContext) => void | Promise<void>
188
+ }
189
+
190
+ export async function makeRequestWithRetry<TValidated>(
191
+ args: MakeRequestArgs<TValidated>
192
+ ): Promise<{
193
+ validatedData: TValidated
194
+ retryCount: number
195
+ httpStatus: number
196
+ }> {
197
+ const {
198
+ config,
199
+ maxRetries,
200
+ retryDelay,
201
+ maxRetryDelayMs,
202
+ validateFn,
203
+ shouldRetry,
204
+ logger,
205
+ ctx,
206
+ endpointName,
207
+ method,
208
+ fullUrl,
209
+ meta,
210
+ onResponse,
211
+ onRetry
212
+ } = args
213
+ let retryCount = 0
214
+
215
+ while (true) {
216
+ let response: Response | undefined
217
+ try {
218
+ logger?.debug?.('API request', config)
219
+ response = await httpRequest(config, (over) =>
220
+ ctx({ attempt: retryCount, ...over })
221
+ )
222
+
223
+ await onResponse?.({
224
+ endpoint: endpointName,
225
+ method,
226
+ url: fullUrl,
227
+ response: response.clone() as Response,
228
+ attempt: retryCount,
229
+ meta
230
+ })
231
+
232
+ if (response.status >= 400) {
233
+ const retryCtx: RetryContext = {
234
+ response,
235
+ retryCount,
236
+ maxRetries,
237
+ endpoint: endpointName,
238
+ method,
239
+ url: fullUrl
240
+ }
241
+ if (retryCount < maxRetries && shouldRetry(retryCtx)) {
242
+ const delayMs = getRetryDelay({
243
+ retryDelay,
244
+ attempt: retryCount,
245
+ response,
246
+ maxRetryDelayMs
247
+ })
248
+ await onRetry?.({
249
+ ...retryCtx,
250
+ delayMs,
251
+ nextAttempt: retryCount + 1
252
+ })
253
+ retryCount++
254
+ await delay(delayMs)
255
+ continue
256
+ }
257
+
258
+ const { body, headers: responseHeaders } =
259
+ await readErrorResponse(response)
260
+ throw new HttpResponseError(
261
+ response.status,
262
+ body,
263
+ responseHeaders,
264
+ ctx({ attempt: retryCount })
265
+ )
266
+ }
267
+
268
+ return {
269
+ validatedData: await validateFn(response, retryCount),
270
+ retryCount,
271
+ httpStatus: response.status
272
+ }
273
+ } catch (error) {
274
+ if (isNonRetryableApiError(error)) {
275
+ throw error
276
+ }
277
+
278
+ const retryCtx: RetryContext = {
279
+ error,
280
+ retryCount,
281
+ maxRetries,
282
+ endpoint: endpointName,
283
+ method,
284
+ url: fullUrl
285
+ }
286
+ if (retryCount >= maxRetries || !shouldRetry(retryCtx)) {
287
+ throw error
288
+ }
289
+
290
+ const delayMs = getRetryDelay({
291
+ retryDelay,
292
+ attempt: retryCount,
293
+ response: undefined,
294
+ maxRetryDelayMs
295
+ })
296
+ await onRetry?.({
297
+ ...retryCtx,
298
+ delayMs,
299
+ nextAttempt: retryCount + 1
300
+ })
301
+ retryCount++
302
+ await delay(delayMs)
303
+ }
304
+ }
305
+ }
306
+
307
+ export async function validateAndTransformResponse<T extends z.ZodType>(
308
+ endpoint: ApiEndpoint<
309
+ z.ZodType | undefined,
310
+ z.ZodType | undefined,
311
+ z.ZodType | undefined,
312
+ z.ZodType | undefined,
313
+ T
314
+ >,
315
+ response: Response,
316
+ ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext
317
+ ): Promise<{ data: z.infer<T>; responseSizeBytes: number }> {
318
+ const responseHeaders = getHeadersAsObject(response.headers)
319
+ const text = await response.text()
320
+ const responseSizeBytes = new TextEncoder().encode(text).length
321
+ let responseData: unknown
322
+ if (hasJsonContentType(responseHeaders)) {
323
+ try {
324
+ responseData = text === '' ? undefined : JSON.parse(text)
325
+ } catch (error) {
326
+ throw new ResponseValidationError(
327
+ makeJsonParseZodError(error, text),
328
+ text,
329
+ ctx()
330
+ )
331
+ }
332
+ } else {
333
+ responseData = text
334
+ }
335
+ const transformedData = endpoint.resFormatter
336
+ ? endpoint.resFormatter(responseData, responseHeaders)
337
+ : responseData
338
+
339
+ const data = await validateResponse(endpoint.resSchema, transformedData, ctx)
340
+ return { data, responseSizeBytes }
341
+ }
342
+
343
+ export function defaultShouldRetry(ctx: RetryContext): boolean {
344
+ if (ctx.error) return shouldAttemptRetryOnError(ctx.error)
345
+ return shouldAttemptRetryOnHttp(ctx)
346
+ }
347
+
348
+ function delay(ms: number): Promise<void> {
349
+ return new Promise((resolve) => setTimeout(resolve, ms))
350
+ }
@@ -0,0 +1,259 @@
1
+ import type { z } from 'zod'
2
+ import type { ApiError } from './errors'
3
+ import type {
4
+ ApiErrorContext,
5
+ BaseApiUrl,
6
+ HttpMethod,
7
+ PathParams,
8
+ QueryParams,
9
+ RequestBodyFormat
10
+ } from './shared'
11
+
12
+ export interface ApiEndpoint<
13
+ TReqBody extends z.ZodType | undefined = z.ZodType | undefined,
14
+ TReqParams extends z.ZodType | undefined = z.ZodType | undefined,
15
+ TReqQuery extends z.ZodType | undefined = z.ZodType | undefined,
16
+ TReqHeaders extends z.ZodType | undefined = z.ZodType | undefined,
17
+ TResponse extends z.ZodType = z.ZodType
18
+ > {
19
+ method: HttpMethod
20
+ path: string | ((baseUrl: string) => string)
21
+ reqBodySchema?: TReqBody
22
+ reqParamsSchema?: TReqParams
23
+ reqQuerySchema?: TReqQuery
24
+ reqHeadersSchema?: TReqHeaders
25
+ resSchema: TResponse
26
+ baseApiUrl?: string
27
+ reqBodyFormat?: RequestBodyFormat
28
+ defaultHeaders?: Record<string, string>
29
+ resFormatter?: (
30
+ data: unknown,
31
+ headers: Record<string, string>
32
+ ) => z.infer<TResponse>
33
+ reqDefaultQueryParams?: QueryParams
34
+ doNotEncodeQueryParams?: boolean
35
+ }
36
+
37
+ export interface ApiWrapperResponse<T> {
38
+ data: T
39
+ requestBodySizeMb: number
40
+ responseSizeMb: number
41
+ responseTimeMs: number
42
+ httpStatus: number
43
+ retryCount: number
44
+ }
45
+
46
+ export type RetryContext = {
47
+ error?: unknown
48
+ response?: Response
49
+ retryCount: number
50
+ maxRetries: number
51
+ endpoint?: string
52
+ method?: HttpMethod
53
+ url?: string
54
+ }
55
+
56
+ export type OnRetryContext = RetryContext & {
57
+ delayMs: number
58
+ nextAttempt: number
59
+ }
60
+
61
+ export type RequestInterceptorResult =
62
+ | {
63
+ headers?: Record<string, string>
64
+ body?: unknown
65
+ }
66
+ | void
67
+ | undefined
68
+
69
+ export type OnRequestContext = {
70
+ endpoint: string
71
+ method: HttpMethod
72
+ url: string
73
+ headers: Record<string, string>
74
+ body: unknown
75
+ meta?: Record<string, unknown>
76
+ }
77
+
78
+ export type OnResponseContext = {
79
+ endpoint: string
80
+ method: HttpMethod
81
+ url: string
82
+ response: Response
83
+ attempt: number
84
+ meta?: Record<string, unknown>
85
+ }
86
+
87
+ export type ActionCache = {
88
+ <T>(
89
+ key: string,
90
+ fn: () => Promise<T> | T,
91
+ options?: { ttlMs?: number }
92
+ ): Promise<T>
93
+ get<T>(key: string): T | undefined
94
+ set<T>(key: string, value: T, options?: { ttlMs?: number }): void
95
+ invalidate(key: string): void
96
+ clear(): void
97
+ }
98
+
99
+ export type ActionInvokeOptions = {
100
+ signal?: AbortSignal
101
+ meta?: Record<string, unknown>
102
+ }
103
+
104
+ // biome-ignore lint/suspicious/noExplicitAny: TApi defaults to any so plain ApiAction stays open
105
+ export type ActionContext<TInput, TApi = any> = {
106
+ input: TInput
107
+ api: TApi
108
+ signal?: AbortSignal
109
+ meta?: Record<string, unknown>
110
+ logger?: Pick<Console, 'debug' | 'error'>
111
+ cache: ActionCache
112
+ }
113
+
114
+ // biome-ignore lint/suspicious/noExplicitAny: variance-friendly bounds
115
+ export interface ApiAction<TInput = any, TOutput = any> {
116
+ handler: (ctx: ActionContext<TInput>) => Promise<TOutput> | TOutput
117
+ /** When true, the action accepts only optional `{ signal, meta }`; input is always `undefined`. */
118
+ readonly noRuntimeInput?: boolean
119
+ }
120
+
121
+ type ActionDefinition<TInput, TOutput, TApi> = {
122
+ handler: (ctx: ActionContext<TInput, TApi>) => Promise<TOutput> | TOutput
123
+ }
124
+
125
+ export type DefineAction<TApi> = {
126
+ <TOutput>(
127
+ def: ActionDefinition<undefined, TOutput, TApi>
128
+ ): ApiAction<undefined, TOutput>
129
+ <TInput>(): <TOutput>(
130
+ def: ActionDefinition<TInput, TOutput, TApi>
131
+ ) => ApiAction<TInput, TOutput>
132
+ }
133
+
134
+ // biome-ignore lint/suspicious/noExplicitAny: variance-friendly inference
135
+ type ActionInput<A> = A extends ApiAction<infer I, any> ? I : never
136
+ // biome-ignore lint/suspicious/noExplicitAny: variance-friendly inference
137
+ export type ActionOutput<A> = A extends ApiAction<any, infer O> ? O : never
138
+
139
+ export type ActionArgs<A> = [ActionInput<A>] extends [undefined]
140
+ ? [options?: ActionInvokeOptions]
141
+ : [input: ActionInput<A>, options?: ActionInvokeOptions]
142
+
143
+ type SchemaInputKeys<TEndpoint extends ApiEndpoint> =
144
+ | (TEndpoint['reqBodySchema'] extends z.ZodType ? 'reqBody' : never)
145
+ | (TEndpoint['reqParamsSchema'] extends z.ZodType ? 'reqParams' : never)
146
+ | (TEndpoint['reqQuerySchema'] extends z.ZodType ? 'reqQuery' : never)
147
+ | (TEndpoint['reqHeadersSchema'] extends z.ZodType ? 'reqHeaders' : never)
148
+
149
+ type SchemaInputs<TEndpoint extends ApiEndpoint> = {
150
+ reqBody: TEndpoint['reqBodySchema'] extends z.ZodType
151
+ ? z.infer<TEndpoint['reqBodySchema']>
152
+ : unknown
153
+ reqParams: TEndpoint['reqParamsSchema'] extends z.ZodType
154
+ ? z.infer<TEndpoint['reqParamsSchema']>
155
+ : PathParams
156
+ reqQuery: TEndpoint['reqQuerySchema'] extends z.ZodType
157
+ ? z.infer<TEndpoint['reqQuerySchema']>
158
+ : QueryParams
159
+ reqHeaders: TEndpoint['reqHeadersSchema'] extends z.ZodType
160
+ ? z.infer<TEndpoint['reqHeadersSchema']>
161
+ : Record<string, string>
162
+ }
163
+
164
+ type CommonRequestOptions = {
165
+ url?: string
166
+ signal?: AbortSignal
167
+ timeoutMs?: number
168
+ retries?: number
169
+ fetch?: typeof fetch
170
+ meta?: Record<string, unknown>
171
+ }
172
+
173
+ export type RequestInput<TEndpoint extends ApiEndpoint> = Pick<
174
+ SchemaInputs<TEndpoint>,
175
+ SchemaInputKeys<TEndpoint>
176
+ > &
177
+ Partial<Omit<SchemaInputs<TEndpoint>, SchemaInputKeys<TEndpoint>>> &
178
+ CommonRequestOptions
179
+
180
+ type RequestArgs<TEndpoint extends ApiEndpoint> =
181
+ SchemaInputKeys<TEndpoint> extends never
182
+ ? [input?: RequestInput<TEndpoint>]
183
+ : [input: RequestInput<TEndpoint>]
184
+
185
+ export type ApiClient<
186
+ T extends Record<string, ApiEndpoint>,
187
+ A extends Record<string, ApiAction> = Record<string, never>
188
+ > = {
189
+ request: {
190
+ <K extends keyof T>(
191
+ endpointKey: K,
192
+ ...args: RequestArgs<T[K]>
193
+ ): Promise<ApiWrapperResponse<z.infer<T[K]['resSchema']>>>
194
+ <K extends keyof A>(
195
+ actionKey: K,
196
+ ...args: ActionArgs<A[K]>
197
+ ): Promise<ActionOutput<A[K]>>
198
+ }
199
+ cache: ActionCache
200
+ } & {
201
+ [K in keyof T]: (
202
+ ...args: RequestArgs<T[K]>
203
+ ) => Promise<ApiWrapperResponse<z.infer<T[K]['resSchema']>>>
204
+ } & {
205
+ [K in keyof A]: (...args: ActionArgs<A[K]>) => Promise<ActionOutput<A[K]>>
206
+ }
207
+
208
+ export type ActionsFactoryHelpers<
209
+ T extends Record<string, ApiEndpoint>,
210
+ A extends Record<string, ApiAction> = Record<string, never>
211
+ > = {
212
+ api: ApiClient<T, A>
213
+ defineAction: DefineAction<ApiClient<T, A>>
214
+ }
215
+
216
+ export interface ApiClientConfig<
217
+ T extends Record<string, ApiEndpoint>,
218
+ A extends Record<string, ApiAction> = Record<string, never>
219
+ > {
220
+ name?: string
221
+ baseApiUrls: Record<string, BaseApiUrl>
222
+ defaultBaseApiUrl?: string
223
+ endpoints: T
224
+ defaultHeaders?: Record<string, string>
225
+ timeoutMs?: number
226
+ retries?: number
227
+ retryDelayMs?: number | ((attempt: number) => number)
228
+ maxRetryDelayMs?: number
229
+ shouldRetry?: (context: RetryContext) => boolean
230
+ logger?: Pick<Console, 'debug' | 'error'>
231
+ fetch?: typeof fetch
232
+ onRequest?: (
233
+ context: OnRequestContext
234
+ ) => RequestInterceptorResult | Promise<RequestInterceptorResult>
235
+ onResponse?: (context: OnResponseContext) => void | Promise<void>
236
+ onError?: (error: ApiError) => void | Promise<void>
237
+ onRetry?: (context: OnRetryContext) => void | Promise<void>
238
+ redact?: (context: ApiErrorContext) => ApiErrorContext
239
+ actions?: A | ((helpers: ActionsFactoryHelpers<T, A>) => A)
240
+ }
241
+
242
+ export type ApiClientConfigNoActions<T extends Record<string, ApiEndpoint>> =
243
+ Omit<ApiClientConfig<T, Record<string, never>>, 'actions'>
244
+
245
+ export type ApiClientConfigWithActions<
246
+ T extends Record<string, ApiEndpoint>,
247
+ A extends Record<string, ApiAction>
248
+ > = Omit<ApiClientConfig<T, Record<string, never>>, 'actions'> & {
249
+ actions: A | ((helpers: ActionsFactoryHelpers<T, A>) => A)
250
+ }
251
+
252
+ export type {
253
+ BaseApiUrl,
254
+ HttpMethod,
255
+ PathParams,
256
+ QueryParams,
257
+ RequestBodyFormat,
258
+ ApiErrorContext
259
+ } from './shared'