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.
- package/README.md +70 -4
- package/dist/api-wrapper/actions-impl.d.ts +4 -0
- package/dist/api-wrapper/builders.d.ts +23 -0
- package/dist/api-wrapper/create-client.d.ts +6 -0
- package/dist/api-wrapper/error-reporting.d.ts +7 -0
- package/dist/api-wrapper/errors.d.ts +64 -0
- package/dist/api-wrapper/headers.d.ts +10 -0
- package/dist/api-wrapper/index.d.ts +5 -84
- package/dist/api-wrapper/redaction.d.ts +2 -0
- package/dist/api-wrapper/shared.d.ts +16 -0
- package/dist/api-wrapper/size.d.ts +1 -0
- package/dist/api-wrapper/test-helpers.d.ts +6 -0
- package/dist/api-wrapper/transport.d.ts +55 -0
- package/dist/api-wrapper/types.d.ts +154 -0
- package/dist/api-wrapper/zod-issues.d.ts +17 -0
- package/dist/cache/index.d.ts +26 -0
- package/dist/secrets/core.d.ts +47 -0
- package/dist/secrets/deadline.d.ts +6 -0
- package/dist/secrets/environment.d.ts +9 -0
- package/dist/secrets/errors.d.ts +11 -0
- package/dist/secrets/index.d.ts +14 -23
- package/dist/secrets/infisical-cli.d.ts +6 -0
- package/dist/secrets/infisical-config.d.ts +17 -0
- package/dist/secrets/infisical-session.d.ts +4 -0
- package/dist/secrets/infisical.d.ts +14 -2
- package/dist/secrets/loaders.d.ts +9 -0
- package/dist/secrets/vite.d.ts +7 -3
- package/dist/secrets/worker-loader.d.ts +9 -0
- package/dist/secrets/worker.d.ts +3 -0
- package/dist/src/api-wrapper/index.js +1059 -177
- package/dist/src/index.js +2807 -1397
- package/dist/src/jwt/index.js +2 -2
- package/dist/src/p-queue/index.js +3 -3
- package/dist/src/secrets/index.js +1610 -407
- package/dist/src/secrets/infisical.js +1352 -214
- package/dist/src/secrets/loaders.js +1529 -0
- package/dist/src/secrets/vite.js +33 -11
- package/dist/src/secrets/worker.js +275 -0
- package/dist/src/sitemap/index.js +1 -1
- package/dist/src/zod/index.js +7 -7
- package/dist/typedswitch/index.d.ts +9 -5
- package/package.json +12 -3
- package/src/api-wrapper/actions-impl.ts +100 -0
- package/src/api-wrapper/builders.ts +200 -0
- package/src/api-wrapper/create-client.ts +393 -0
- package/src/api-wrapper/error-reporting.ts +31 -0
- package/src/api-wrapper/errors.ts +231 -0
- package/src/api-wrapper/headers.ts +79 -0
- package/src/api-wrapper/index.ts +5 -492
- package/src/api-wrapper/redaction.ts +62 -0
- package/src/api-wrapper/shared.ts +25 -0
- package/src/api-wrapper/size.ts +19 -0
- package/src/api-wrapper/test-helpers.ts +25 -0
- package/src/api-wrapper/transport.ts +350 -0
- package/src/api-wrapper/types.ts +259 -0
- package/src/api-wrapper/zod-issues.ts +157 -0
- package/src/cache/index.ts +228 -61
- package/src/secrets/core.ts +348 -0
- package/src/secrets/deadline.ts +69 -0
- package/src/secrets/environment.ts +69 -0
- package/src/secrets/errors.ts +29 -0
- package/src/secrets/index.ts +81 -140
- package/src/secrets/infisical-cli.ts +99 -0
- package/src/secrets/infisical-config.ts +70 -0
- package/src/secrets/infisical-session.ts +42 -0
- package/src/secrets/infisical.ts +219 -35
- package/src/secrets/loaders.ts +43 -0
- package/src/secrets/vite.ts +44 -15
- package/src/secrets/worker-loader.ts +56 -0
- package/src/secrets/worker.ts +4 -0
- 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'
|