hanzio 1.1.0 → 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 (55) hide show
  1. package/README.md +70 -4
  2. package/dist/api-wrapper/builders.d.ts +13 -2
  3. package/dist/api-wrapper/errors.d.ts +1 -0
  4. package/dist/api-wrapper/headers.d.ts +2 -1
  5. package/dist/api-wrapper/transport.d.ts +8 -5
  6. package/dist/api-wrapper/types.d.ts +1 -0
  7. package/dist/cache/index.d.ts +26 -0
  8. package/dist/secrets/core.d.ts +47 -0
  9. package/dist/secrets/deadline.d.ts +6 -0
  10. package/dist/secrets/environment.d.ts +9 -0
  11. package/dist/secrets/errors.d.ts +11 -0
  12. package/dist/secrets/index.d.ts +13 -25
  13. package/dist/secrets/infisical-cli.d.ts +6 -0
  14. package/dist/secrets/infisical-config.d.ts +17 -0
  15. package/dist/secrets/infisical-session.d.ts +4 -0
  16. package/dist/secrets/infisical.d.ts +8 -13
  17. package/dist/secrets/loaders.d.ts +5 -12
  18. package/dist/secrets/vite.d.ts +7 -3
  19. package/dist/secrets/worker-loader.d.ts +9 -0
  20. package/dist/secrets/worker.d.ts +3 -0
  21. package/dist/src/api-wrapper/index.js +152 -122
  22. package/dist/src/index.js +3173 -2713
  23. package/dist/src/jwt/index.js +2 -2
  24. package/dist/src/p-queue/index.js +3 -3
  25. package/dist/src/secrets/index.js +670 -402
  26. package/dist/src/secrets/infisical.js +451 -168
  27. package/dist/src/secrets/loaders.js +467 -174
  28. package/dist/src/secrets/vite.js +33 -11
  29. package/dist/src/secrets/worker.js +275 -0
  30. package/dist/src/sitemap/index.js +1 -1
  31. package/dist/src/zod/index.js +7 -7
  32. package/dist/typedswitch/index.d.ts +9 -5
  33. package/package.json +12 -3
  34. package/src/api-wrapper/builders.ts +68 -43
  35. package/src/api-wrapper/create-client.ts +82 -62
  36. package/src/api-wrapper/errors.ts +10 -0
  37. package/src/api-wrapper/headers.ts +14 -1
  38. package/src/api-wrapper/redaction.ts +1 -0
  39. package/src/api-wrapper/transport.ts +34 -38
  40. package/src/api-wrapper/types.ts +1 -0
  41. package/src/cache/index.ts +228 -61
  42. package/src/secrets/core.ts +348 -0
  43. package/src/secrets/deadline.ts +69 -0
  44. package/src/secrets/environment.ts +69 -0
  45. package/src/secrets/errors.ts +29 -0
  46. package/src/secrets/index.ts +79 -210
  47. package/src/secrets/infisical-cli.ts +99 -0
  48. package/src/secrets/infisical-config.ts +70 -0
  49. package/src/secrets/infisical-session.ts +42 -0
  50. package/src/secrets/infisical.ts +214 -66
  51. package/src/secrets/loaders.ts +24 -48
  52. package/src/secrets/vite.ts +44 -15
  53. package/src/secrets/worker-loader.ts +56 -0
  54. package/src/secrets/worker.ts +4 -0
  55. package/src/typedswitch/index.ts +80 -69
@@ -15,6 +15,7 @@ import type { PathParams, QueryParams } from './shared'
15
15
  import type { ApiErrorContext } from './shared'
16
16
  import {
17
17
  buildHeaders,
18
+ buildPathParams,
18
19
  buildQueryParams,
19
20
  buildRequestBody,
20
21
  buildUrl
@@ -51,6 +52,39 @@ function isActionInvokeOptionsOnly(
51
52
  )
52
53
  }
53
54
 
55
+ function resolveActionInvocation(
56
+ action: AnyApiAction,
57
+ actionName: string,
58
+ first: unknown,
59
+ second?: ActionInvokeOptions
60
+ ): { input: unknown; options: ActionInvokeOptions } {
61
+ if (!action.noRuntimeInput) {
62
+ return { input: first, options: second ?? {} }
63
+ }
64
+
65
+ if (first === undefined || first === null) {
66
+ return { input: undefined, options: second ?? {} }
67
+ }
68
+ if (second !== undefined) {
69
+ return { input: undefined, options: second }
70
+ }
71
+ if (isActionInvokeOptionsOnly(first)) {
72
+ return { input: undefined, options: first }
73
+ }
74
+
75
+ throw new ConfigError(
76
+ `Action "${actionName}" does not accept input; pass only optional { signal, meta }`,
77
+ {
78
+ endpoint: actionName,
79
+ method: 'GET',
80
+ url: '',
81
+ attempt: 0,
82
+ maxRetries: 0,
83
+ elapsedMs: 0
84
+ }
85
+ )
86
+ }
87
+
54
88
  function createApiClientImpl<
55
89
  T extends Record<string, ApiEndpoint>,
56
90
  A extends Record<string, ApiAction> = Record<string, never>
@@ -102,31 +136,12 @@ function createApiClientImpl<
102
136
 
103
137
  baseContext.method = endpoint.method
104
138
 
105
- let finalUrl: string
106
- let fullUrl: string
107
- try {
108
- const built = buildUrl(
109
- endpoint,
110
- rawInput.reqParams,
111
- rawInput.reqQuery,
112
- apiConfig.baseApiUrls,
113
- apiConfig.defaultBaseApiUrl,
114
- input.url,
115
- (msg) => new ConfigError(msg, ctx())
116
- )
117
- finalUrl = built.finalUrl
118
- fullUrl = built.fullUrl
119
- } catch (e) {
120
- if (e instanceof ApiError) throw await reportApiError(e, apiConfig)
121
- throw e
122
- }
123
-
124
- baseContext.url = fullUrl
125
-
139
+ let pathParams: PathParams | undefined
126
140
  let queryParams: Record<string, string>
127
141
  let body: unknown
128
142
  let headers: Record<string, string>
129
143
  try {
144
+ pathParams = buildPathParams(endpoint, rawInput.reqParams, ctx)
130
145
  queryParams = buildQueryParams(endpoint, rawInput.reqQuery, ctx)
131
146
  body = buildRequestBody(endpoint, rawInput.reqBody, ctx)
132
147
  headers = buildHeaders(
@@ -140,6 +155,25 @@ function createApiClientImpl<
140
155
  throw e
141
156
  }
142
157
 
158
+ let fullUrl: string
159
+ try {
160
+ const built = buildUrl({
161
+ endpoint,
162
+ params: pathParams,
163
+ queryParams,
164
+ baseApiUrls: apiConfig.baseApiUrls,
165
+ defaultBaseApiUrl: apiConfig.defaultBaseApiUrl,
166
+ url: input.url,
167
+ doNotEncodeQueryParams: endpoint.doNotEncodeQueryParams,
168
+ configError: (msg) => new ConfigError(msg, ctx())
169
+ })
170
+ fullUrl = built.fullUrl
171
+ } catch (e) {
172
+ if (e instanceof ApiError) throw await reportApiError(e, apiConfig)
173
+ throw e
174
+ }
175
+
176
+ baseContext.url = fullUrl
143
177
  baseContext.requestHeaders = headers
144
178
  baseContext.requestBody = body
145
179
 
@@ -163,11 +197,9 @@ function createApiClientImpl<
163
197
 
164
198
  const config: RequestConfig = {
165
199
  method: endpoint.method,
166
- url: finalUrl,
167
- queryParams,
200
+ fullUrl,
168
201
  body,
169
202
  headers,
170
- doNotEncodeQueryParams: endpoint.doNotEncodeQueryParams,
171
203
  timeoutMs: input.timeoutMs ?? apiConfig.timeoutMs,
172
204
  fetchFn: input.fetch ?? apiConfig.fetch ?? fetch,
173
205
  userSignal: input.signal
@@ -178,8 +210,11 @@ function createApiClientImpl<
178
210
  config,
179
211
  maxRetries,
180
212
  retryDelay: apiConfig.retryDelayMs ?? 300,
181
- validateFn: (response) =>
182
- validateAndTransformResponse(endpoint, response, ctx),
213
+ maxRetryDelayMs: apiConfig.maxRetryDelayMs,
214
+ validateFn: (response, attempt) =>
215
+ validateAndTransformResponse(endpoint, response, (over) =>
216
+ ctx({ attempt, ...over })
217
+ ),
183
218
  shouldRetry: apiConfig.shouldRetry ?? defaultShouldRetry,
184
219
  logger: apiConfig.logger,
185
220
  ctx,
@@ -192,11 +227,12 @@ function createApiClientImpl<
192
227
  })
193
228
 
194
229
  const responseTimeMs = elapsed()
195
- const responseSizeMb = calculateSizeInMb(result.validatedData)
230
+ const responseSizeMb =
231
+ result.validatedData.responseSizeBytes / (1024 * 1024)
196
232
  const requestBodySizeMb = calculateSizeInMb(config.body)
197
233
 
198
234
  return {
199
- data: result.validatedData as z.infer<T[K]['resSchema']>,
235
+ data: result.validatedData.data as z.infer<T[K]['resSchema']>,
200
236
  requestBodySizeMb,
201
237
  responseSizeMb,
202
238
  responseTimeMs,
@@ -264,26 +300,28 @@ function createApiClientImpl<
264
300
  }
265
301
  }
266
302
 
303
+ async function dispatchAction(
304
+ name: string,
305
+ first?: unknown,
306
+ second?: ActionInvokeOptions
307
+ ): Promise<unknown> {
308
+ const action = actions[name]!
309
+ const { input, options } = resolveActionInvocation(
310
+ action,
311
+ name,
312
+ first,
313
+ second
314
+ )
315
+ return invokeAction(name, input, options)
316
+ }
317
+
267
318
  const request = (async (
268
319
  key: string,
269
320
  input?: unknown,
270
321
  options?: ActionInvokeOptions
271
322
  ): Promise<unknown> => {
272
323
  if (key in actions) {
273
- const action = actions[key]!
274
- if (action.noRuntimeInput) {
275
- if (input === undefined || input === null) {
276
- return invokeAction(key, undefined, options ?? {})
277
- }
278
- if (options !== undefined) {
279
- return invokeAction(key, undefined, options)
280
- }
281
- if (isActionInvokeOptionsOnly(input)) {
282
- return invokeAction(key, undefined, input)
283
- }
284
- return invokeAction(key, undefined, {})
285
- }
286
- return invokeAction(key, input, options)
324
+ return dispatchAction(key, input, options)
287
325
  }
288
326
  return invokeEndpoint(
289
327
  key as keyof T,
@@ -305,26 +343,8 @@ function createApiClientImpl<
305
343
  )
306
344
  }
307
345
  if (prop in actions) {
308
- const action = actions[prop]!
309
- if (action.noRuntimeInput) {
310
- return (
311
- first?: unknown,
312
- second?: ActionInvokeOptions
313
- ): Promise<unknown> => {
314
- if (first === undefined || first === null) {
315
- return invokeAction(prop, undefined, second ?? {})
316
- }
317
- if (second !== undefined) {
318
- return invokeAction(prop, undefined, second)
319
- }
320
- if (isActionInvokeOptionsOnly(first)) {
321
- return invokeAction(prop, undefined, first)
322
- }
323
- return invokeAction(prop, undefined, {})
324
- }
325
- }
326
- return (input?: unknown, opt?: ActionInvokeOptions) =>
327
- invokeAction(prop, input, opt)
346
+ return (first?: unknown, second?: ActionInvokeOptions) =>
347
+ dispatchAction(prop, first, second)
328
348
  }
329
349
  return undefined
330
350
  }
@@ -219,3 +219,13 @@ export const isConfigError = (e: unknown): e is ConfigError =>
219
219
  e instanceof ConfigError
220
220
  export const isActionError = (e: unknown): e is ActionError =>
221
221
  e instanceof ActionError
222
+
223
+ export function isNonRetryableApiError(error: unknown): boolean {
224
+ return (
225
+ error instanceof HttpResponseError ||
226
+ error instanceof ResponseValidationError ||
227
+ error instanceof RequestValidationError ||
228
+ error instanceof ConfigError ||
229
+ error instanceof RequestAbortedError
230
+ )
231
+ }
@@ -1,3 +1,5 @@
1
+ import type { ConfigError } from './errors'
2
+
1
3
  const CANONICAL_HEADER_NAMES: Record<string, string> = {
2
4
  'content-type': 'Content-Type',
3
5
  authorization: 'Authorization',
@@ -49,11 +51,22 @@ export function hasJsonContentType(headers: Record<string, string>): boolean {
49
51
 
50
52
  export function buildFetchBody(
51
53
  body: unknown,
52
- headers: Record<string, string>
54
+ headers: Record<string, string>,
55
+ configError?: (msg: string) => ConfigError
53
56
  ): RequestInit['body'] {
54
57
  if (body === undefined || body === null) return undefined
55
58
  if (body instanceof FormData || body instanceof Blob) return body
56
59
  if (hasJsonContentType(headers)) return JSON.stringify(body)
60
+ if (
61
+ typeof body === 'object' &&
62
+ (Object.getPrototypeOf(body) === Object.prototype ||
63
+ Object.getPrototypeOf(body) === null) &&
64
+ configError
65
+ ) {
66
+ throw configError(
67
+ 'Request body is a plain object but Content-Type is not application/json'
68
+ )
69
+ }
57
70
  return body as RequestInit['body']
58
71
  }
59
72
 
@@ -17,6 +17,7 @@ const SENSITIVE_BODY_KEYS = new Set([
17
17
  'refresh_token',
18
18
  'secret',
19
19
  'client_secret',
20
+ 'clientsecret',
20
21
  'apikey',
21
22
  'api_key'
22
23
  ])
@@ -9,10 +9,10 @@ import type { ApiErrorContext, HttpMethod } from './shared'
9
9
  import {
10
10
  ConfigError,
11
11
  HttpResponseError,
12
+ isNonRetryableApiError,
12
13
  NetworkError,
13
14
  RequestAbortedError,
14
15
  RequestTimeoutError,
15
- RequestValidationError,
16
16
  ResponseValidationError
17
17
  } from './errors'
18
18
  import {
@@ -24,11 +24,9 @@ import { makeJsonParseZodError } from './zod-issues'
24
24
 
25
25
  export type RequestConfig = {
26
26
  method: HttpMethod
27
- url: string
28
- queryParams: Record<string, string>
27
+ fullUrl: string
29
28
  body: unknown
30
29
  headers: Record<string, string>
31
- doNotEncodeQueryParams?: boolean
32
30
  timeoutMs?: number
33
31
  fetchFn: typeof fetch
34
32
  userSignal?: AbortSignal
@@ -53,13 +51,6 @@ export async function httpRequest(
53
51
  config: RequestConfig,
54
52
  ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext
55
53
  ): Promise<Response> {
56
- const queryString = config.doNotEncodeQueryParams
57
- ? Object.entries(config.queryParams)
58
- .map(([key, value]) => `${key}=${value}`)
59
- .join('&')
60
- : new URLSearchParams(config.queryParams).toString()
61
- const fullUrl = queryString ? `${config.url}?${queryString}` : config.url
62
-
63
54
  const timeoutController = new AbortController()
64
55
  const timeout =
65
56
  config.timeoutMs === undefined
@@ -69,10 +60,14 @@ export async function httpRequest(
69
60
  const signal = mergeSignals(timeoutController.signal, config.userSignal)
70
61
 
71
62
  try {
72
- return await config.fetchFn(fullUrl, {
63
+ return await config.fetchFn(config.fullUrl, {
73
64
  method: config.method,
74
65
  headers: config.headers,
75
- body: buildFetchBody(config.body, config.headers),
66
+ body: buildFetchBody(
67
+ config.body,
68
+ config.headers,
69
+ (msg) => new ConfigError(msg, ctx())
70
+ ),
76
71
  signal
77
72
  })
78
73
  } catch (error) {
@@ -85,6 +80,7 @@ export async function httpRequest(
85
80
  }
86
81
  throw new RequestAbortedError(ctx(), error)
87
82
  }
83
+ if (error instanceof ConfigError) throw error
88
84
  throw new NetworkError(
89
85
  error instanceof Error ? error.message : String(error),
90
86
  ctx(),
@@ -142,30 +138,30 @@ export function getRetryDelay(params: {
142
138
  retryDelay: number | ((attempt: number) => number)
143
139
  attempt: number
144
140
  response?: Response
141
+ maxRetryDelayMs?: number
145
142
  }): number {
143
+ const maxDelay = params.maxRetryDelayMs ?? 30_000
146
144
  const { retryDelay, attempt, response } = params
147
145
  if (response && (response.status === 429 || response.status === 503)) {
148
146
  const retryAfter = response.headers.get('retry-after')
149
147
  if (retryAfter) {
150
148
  const seconds = Number(retryAfter)
151
- if (Number.isFinite(seconds)) return seconds * 1000
149
+ if (Number.isFinite(seconds)) {
150
+ return Math.min(Math.max(0, seconds * 1000), maxDelay)
151
+ }
152
152
  const dateMs = Date.parse(retryAfter)
153
153
  if (!Number.isNaN(dateMs)) {
154
- return Math.max(0, dateMs - Date.now())
154
+ return Math.min(Math.max(0, dateMs - Date.now()), maxDelay)
155
155
  }
156
156
  }
157
157
  }
158
- if (typeof retryDelay === 'function') return retryDelay(attempt)
159
- return retryDelay
158
+ const delayMs =
159
+ typeof retryDelay === 'function' ? retryDelay(attempt) : retryDelay
160
+ return Math.min(delayMs, maxDelay)
160
161
  }
161
162
 
162
163
  export function shouldAttemptRetryOnError(error: unknown): boolean {
163
- if (error instanceof HttpResponseError) return false
164
- if (error instanceof ResponseValidationError) return false
165
- if (error instanceof RequestValidationError) return false
166
- if (error instanceof ConfigError) return false
167
- if (error instanceof RequestAbortedError) return false
168
- return true
164
+ return !isNonRetryableApiError(error)
169
165
  }
170
166
 
171
167
  export function shouldAttemptRetryOnHttp(retryCtx: RetryContext): boolean {
@@ -178,7 +174,8 @@ type MakeRequestArgs<TValidated> = {
178
174
  config: RequestConfig
179
175
  maxRetries: number
180
176
  retryDelay: number | ((attempt: number) => number)
181
- validateFn: (response: Response) => Promise<TValidated>
177
+ maxRetryDelayMs?: number
178
+ validateFn: (response: Response, attempt: number) => Promise<TValidated>
182
179
  shouldRetry: (context: RetryContext) => boolean
183
180
  logger?: Pick<Console, 'debug'>
184
181
  ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext
@@ -201,6 +198,7 @@ export async function makeRequestWithRetry<TValidated>(
201
198
  config,
202
199
  maxRetries,
203
200
  retryDelay,
201
+ maxRetryDelayMs,
204
202
  validateFn,
205
203
  shouldRetry,
206
204
  logger,
@@ -244,7 +242,8 @@ export async function makeRequestWithRetry<TValidated>(
244
242
  const delayMs = getRetryDelay({
245
243
  retryDelay,
246
244
  attempt: retryCount,
247
- response
245
+ response,
246
+ maxRetryDelayMs
248
247
  })
249
248
  await onRetry?.({
250
249
  ...retryCtx,
@@ -267,18 +266,12 @@ export async function makeRequestWithRetry<TValidated>(
267
266
  }
268
267
 
269
268
  return {
270
- validatedData: await validateFn(response),
269
+ validatedData: await validateFn(response, retryCount),
271
270
  retryCount,
272
271
  httpStatus: response.status
273
272
  }
274
273
  } catch (error) {
275
- if (
276
- error instanceof HttpResponseError ||
277
- error instanceof ResponseValidationError ||
278
- error instanceof RequestValidationError ||
279
- error instanceof ConfigError ||
280
- error instanceof RequestAbortedError
281
- ) {
274
+ if (isNonRetryableApiError(error)) {
282
275
  throw error
283
276
  }
284
277
 
@@ -297,7 +290,8 @@ export async function makeRequestWithRetry<TValidated>(
297
290
  const delayMs = getRetryDelay({
298
291
  retryDelay,
299
292
  attempt: retryCount,
300
- response: undefined
293
+ response: undefined,
294
+ maxRetryDelayMs
301
295
  })
302
296
  await onRetry?.({
303
297
  ...retryCtx,
@@ -320,11 +314,12 @@ export async function validateAndTransformResponse<T extends z.ZodType>(
320
314
  >,
321
315
  response: Response,
322
316
  ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext
323
- ): Promise<z.infer<T>> {
317
+ ): Promise<{ data: z.infer<T>; responseSizeBytes: number }> {
324
318
  const responseHeaders = getHeadersAsObject(response.headers)
319
+ const text = await response.text()
320
+ const responseSizeBytes = new TextEncoder().encode(text).length
325
321
  let responseData: unknown
326
322
  if (hasJsonContentType(responseHeaders)) {
327
- const text = await response.text()
328
323
  try {
329
324
  responseData = text === '' ? undefined : JSON.parse(text)
330
325
  } catch (error) {
@@ -335,13 +330,14 @@ export async function validateAndTransformResponse<T extends z.ZodType>(
335
330
  )
336
331
  }
337
332
  } else {
338
- responseData = await response.text()
333
+ responseData = text
339
334
  }
340
335
  const transformedData = endpoint.resFormatter
341
336
  ? endpoint.resFormatter(responseData, responseHeaders)
342
337
  : responseData
343
338
 
344
- return validateResponse(endpoint.resSchema, transformedData, ctx)
339
+ const data = await validateResponse(endpoint.resSchema, transformedData, ctx)
340
+ return { data, responseSizeBytes }
345
341
  }
346
342
 
347
343
  export function defaultShouldRetry(ctx: RetryContext): boolean {
@@ -225,6 +225,7 @@ export interface ApiClientConfig<
225
225
  timeoutMs?: number
226
226
  retries?: number
227
227
  retryDelayMs?: number | ((attempt: number) => number)
228
+ maxRetryDelayMs?: number
228
229
  shouldRetry?: (context: RetryContext) => boolean
229
230
  logger?: Pick<Console, 'debug' | 'error'>
230
231
  fetch?: typeof fetch