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
@@ -1,492 +1,5 @@
1
- import { type z, ZodError } from 'zod'
2
-
3
- export type HttpMethod =
4
- | 'GET'
5
- | 'POST'
6
- | 'PUT'
7
- | 'DELETE'
8
- | 'PATCH'
9
- | 'HEAD'
10
- | 'OPTIONS'
11
-
12
- export type RequestBodyFormat = 'json' | 'form-data'
13
- export type BaseApiUrl = string | (() => string)
14
- export type PathParams = Record<string, string | number>
15
- export type QueryParams = Record<string, string | number | boolean | undefined>
16
-
17
- export class ResponseValidationError extends Error {
18
- public readonly zodError: ZodError
19
- public readonly rawResponse: unknown
20
- public readonly validationIssues: string
21
-
22
- constructor(zodError: ZodError, rawResponse: unknown) {
23
- const issues = zodError.issues
24
- .map((issue) => `${issue.path.join('.') || '<root>'}: ${issue.message}`)
25
- .join('; ')
26
-
27
- super(`Response validation failed: ${issues}`)
28
- this.name = 'ResponseValidationError'
29
- this.zodError = zodError
30
- this.rawResponse = rawResponse
31
- this.validationIssues = issues
32
- }
33
- }
34
-
35
- export class HttpResponseError extends Error {
36
- public readonly status: number
37
- public readonly body: string
38
-
39
- constructor(status: number, body: string) {
40
- super(`HTTP error: ${status} - ${body}`)
41
- this.name = 'HttpResponseError'
42
- this.status = status
43
- this.body = body
44
- }
45
- }
46
-
47
- export interface ApiEndpoint<
48
- TReqBody extends z.ZodType | undefined = z.ZodType | undefined,
49
- TReqParams extends z.ZodType | undefined = z.ZodType | undefined,
50
- TReqQuery extends z.ZodType | undefined = z.ZodType | undefined,
51
- TReqHeaders extends z.ZodType | undefined = z.ZodType | undefined,
52
- TResponse extends z.ZodType = z.ZodType
53
- > {
54
- method: HttpMethod
55
- path: string | ((baseUrl: string) => string)
56
- reqBodySchema?: TReqBody
57
- reqParamsSchema?: TReqParams
58
- reqQuerySchema?: TReqQuery
59
- reqHeadersSchema?: TReqHeaders
60
- resSchema: TResponse
61
- baseApiUrl?: string
62
- reqBodyFormat?: RequestBodyFormat
63
- defaultHeaders?: Record<string, string>
64
- resFormatter?: (
65
- data: unknown,
66
- headers: Record<string, string>
67
- ) => z.infer<TResponse>
68
- reqDefaultQueryParams?: QueryParams
69
- doNotEncodeQueryParams?: boolean
70
- }
71
-
72
- export interface ApiWrapperResponse<T> {
73
- data: T
74
- requestSizeMb: number
75
- responseSizeMb: number
76
- requestSizeTotalMb: number
77
- responseTimeMs: number
78
- httpStatus: number
79
- retryCount: number
80
- }
81
-
82
- export type RetryContext = {
83
- error?: unknown
84
- response?: Response
85
- retryCount: number
86
- maxRetries: number
87
- }
88
-
89
- export interface ApiClientConfig<T extends Record<string, ApiEndpoint>> {
90
- name?: string
91
- baseApiUrls: Record<string, BaseApiUrl>
92
- defaultBaseApiUrl?: string
93
- endpoints: T
94
- defaultHeaders?: Record<string, string>
95
- timeoutMs?: number
96
- retries?: number
97
- retryDelayMs?: number
98
- shouldRetry?: (context: RetryContext) => boolean
99
- logger?: Pick<Console, 'debug' | 'error'>
100
- fetch?: typeof fetch
101
- onRequestStart?: (context: { endpoint: string; url: string }) => void
102
- onRequestEnd?: (context: {
103
- endpoint: string
104
- url: string
105
- status: number
106
- retryCount: number
107
- responseTimeMs: number
108
- }) => void
109
- }
110
-
111
- type RequestInput<TEndpoint extends ApiEndpoint> = {
112
- reqBody?: InferOptionalSchema<TEndpoint['reqBodySchema']>
113
- reqParams?: InferOptionalSchema<TEndpoint['reqParamsSchema']>
114
- reqQuery?: InferOptionalSchema<TEndpoint['reqQuerySchema']>
115
- reqHeaders?: InferOptionalSchema<TEndpoint['reqHeadersSchema']>
116
- url?: string
117
- }
118
-
119
- type InferOptionalSchema<TSchema> = TSchema extends z.ZodType
120
- ? z.infer<TSchema>
121
- : undefined
122
-
123
- type RequestConfig = {
124
- method: HttpMethod
125
- url: string
126
- queryParams: Record<string, string>
127
- body: unknown
128
- headers: Record<string, string>
129
- doNotEncodeQueryParams?: boolean
130
- timeoutMs?: number
131
- fetchFn: typeof fetch
132
- }
133
-
134
- export function createApiClient<T extends Record<string, ApiEndpoint>>(
135
- apiConfig: ApiClientConfig<T>
136
- ) {
137
- const request = async <K extends keyof T>(
138
- endpointKey: K,
139
- input: RequestInput<T[K]> = {}
140
- ): Promise<ApiWrapperResponse<z.infer<T[K]['resSchema']>>> => {
141
- const endpoint = apiConfig.endpoints[endpointKey]
142
-
143
- if (!endpoint) {
144
- throw new Error(`Unknown API endpoint: ${String(endpointKey)}`)
145
- }
146
-
147
- const { finalUrl, fullUrl } = buildUrl(
148
- endpoint,
149
- input.reqParams as PathParams | undefined,
150
- input.reqQuery as QueryParams | undefined,
151
- apiConfig.baseApiUrls,
152
- apiConfig.defaultBaseApiUrl,
153
- input.url
154
- )
155
- const endpointName = apiConfig.name
156
- ? `${apiConfig.name}.${String(endpointKey)}`
157
- : String(endpointKey)
158
- const startedAt = Date.now()
159
- let retryCount = 0
160
- let httpStatus = 0
161
-
162
- apiConfig.onRequestStart?.({ endpoint: endpointName, url: fullUrl })
163
-
164
- try {
165
- const config: RequestConfig = {
166
- method: endpoint.method,
167
- url: finalUrl,
168
- queryParams: buildQueryParams(
169
- endpoint,
170
- input.reqQuery as QueryParams | undefined
171
- ),
172
- body: buildRequestBody(endpoint, input.reqBody),
173
- headers: buildHeaders(
174
- endpoint,
175
- input.reqHeaders as Record<string, string> | undefined,
176
- apiConfig.defaultHeaders ?? {}
177
- ),
178
- doNotEncodeQueryParams: endpoint.doNotEncodeQueryParams,
179
- timeoutMs: apiConfig.timeoutMs,
180
- fetchFn: apiConfig.fetch ?? fetch
181
- }
182
-
183
- const result = await makeRequestWithRetry(
184
- config,
185
- apiConfig.retries ?? 3,
186
- apiConfig.retryDelayMs ?? 300,
187
- (response) => validateAndTransformResponse(endpoint, response),
188
- apiConfig.shouldRetry ?? defaultShouldRetry,
189
- apiConfig.logger
190
- )
191
-
192
- retryCount = result.retryCount
193
- httpStatus = result.httpStatus
194
-
195
- const responseTimeMs = Date.now() - startedAt
196
- const responseSizeMb = calculateSizeInMb(result.validatedData)
197
- const requestSizeMb = calculateSizeInMb({
198
- method: config.method,
199
- url: fullUrl,
200
- body: config.body,
201
- headers: config.headers
202
- })
203
-
204
- apiConfig.onRequestEnd?.({
205
- endpoint: endpointName,
206
- url: fullUrl,
207
- status: httpStatus,
208
- retryCount,
209
- responseTimeMs
210
- })
211
-
212
- return {
213
- data: result.validatedData as z.infer<T[K]['resSchema']>,
214
- requestSizeMb,
215
- responseSizeMb,
216
- requestSizeTotalMb: requestSizeMb + responseSizeMb,
217
- responseTimeMs,
218
- httpStatus,
219
- retryCount
220
- }
221
- } catch (error) {
222
- apiConfig.logger?.error?.(error)
223
- throw error
224
- }
225
- }
226
-
227
- return { request }
228
- }
229
-
230
- export const createApiWrapper = createApiClient
231
-
232
- async function validateResponse<T extends z.ZodType>(
233
- schema: T,
234
- data: unknown
235
- ): Promise<z.infer<T>> {
236
- try {
237
- return await schema.parseAsync(data)
238
- } catch (error) {
239
- if (error instanceof ZodError) {
240
- throw new ResponseValidationError(error, data)
241
- }
242
- throw error
243
- }
244
- }
245
-
246
- async function httpRequest(config: RequestConfig): Promise<Response> {
247
- const queryString = config.doNotEncodeQueryParams
248
- ? Object.entries(config.queryParams)
249
- .map(([key, value]) => `${key}=${value}`)
250
- .join('&')
251
- : new URLSearchParams(config.queryParams).toString()
252
- const fullUrl = queryString ? `${config.url}?${queryString}` : config.url
253
- const abortController = new AbortController()
254
- const timeout =
255
- config.timeoutMs === undefined
256
- ? undefined
257
- : setTimeout(() => abortController.abort(), config.timeoutMs)
258
-
259
- try {
260
- return await config.fetchFn(fullUrl, {
261
- method: config.method,
262
- headers: config.headers,
263
- body: buildFetchBody(config.body, config.headers),
264
- signal: abortController.signal
265
- })
266
- } finally {
267
- if (timeout) clearTimeout(timeout)
268
- }
269
- }
270
-
271
- async function makeRequestWithRetry<TValidated>(
272
- config: RequestConfig,
273
- maxRetries: number,
274
- retryDelayMs: number,
275
- validateFn: (response: Response) => Promise<TValidated>,
276
- shouldRetry: (context: RetryContext) => boolean,
277
- logger?: Pick<Console, 'debug'>
278
- ): Promise<{
279
- validatedData: TValidated
280
- retryCount: number
281
- httpStatus: number
282
- }> {
283
- let retryCount = 0
284
-
285
- while (true) {
286
- try {
287
- logger?.debug?.('API request', config)
288
- const response = await httpRequest(config)
289
-
290
- if (response.status >= 400) {
291
- if (
292
- retryCount < maxRetries &&
293
- shouldRetry({ response, retryCount, maxRetries })
294
- ) {
295
- retryCount++
296
- await delay(retryDelayMs)
297
- continue
298
- }
299
-
300
- throw new HttpResponseError(response.status, await response.text())
301
- }
302
-
303
- return {
304
- validatedData: await validateFn(response),
305
- retryCount,
306
- httpStatus: response.status
307
- }
308
- } catch (error) {
309
- if (error instanceof HttpResponseError) throw error
310
-
311
- if (
312
- retryCount >= maxRetries ||
313
- !shouldRetry({ error, retryCount, maxRetries })
314
- ) {
315
- throw error
316
- }
317
-
318
- retryCount++
319
- await delay(retryDelayMs)
320
- }
321
- }
322
- }
323
-
324
- async function validateAndTransformResponse<T extends z.ZodType>(
325
- endpoint: ApiEndpoint<
326
- z.ZodType | undefined,
327
- z.ZodType | undefined,
328
- z.ZodType | undefined,
329
- z.ZodType | undefined,
330
- T
331
- >,
332
- response: Response
333
- ): Promise<z.infer<T>> {
334
- const responseHeaders = getHeadersAsObject(response.headers)
335
- const contentType = responseHeaders['content-type'] ?? ''
336
- const responseData: unknown = contentType.includes('application/json')
337
- ? await response.json()
338
- : await response.text()
339
- const transformedData = endpoint.resFormatter
340
- ? endpoint.resFormatter(responseData, responseHeaders)
341
- : responseData
342
-
343
- return validateResponse(endpoint.resSchema, transformedData)
344
- }
345
-
346
- function buildUrl(
347
- endpoint: ApiEndpoint,
348
- params: PathParams | undefined,
349
- reqQuery: QueryParams | undefined,
350
- baseApiUrls: Record<string, BaseApiUrl>,
351
- defaultBaseApiUrl?: string,
352
- url?: string
353
- ) {
354
- if (url) {
355
- const finalUrl = replacePathParams(url, params)
356
- const queryString = new URLSearchParams(
357
- buildQueryParams(endpoint, reqQuery)
358
- ).toString()
359
- return {
360
- finalUrl,
361
- fullUrl: queryString ? `${finalUrl}?${queryString}` : finalUrl
362
- }
363
- }
364
-
365
- const defaultBaseUrl = defaultBaseApiUrl ?? Object.keys(baseApiUrls)[0]
366
- if (!defaultBaseUrl) {
367
- throw new Error('At least one base API URL is required')
368
- }
369
-
370
- const baseUrlKey = endpoint.baseApiUrl ?? defaultBaseUrl
371
- const configBaseUrl = baseApiUrls[baseUrlKey]
372
- if (!configBaseUrl) {
373
- throw new Error(`Unknown base API URL: ${baseUrlKey}`)
374
- }
375
-
376
- const baseUrl =
377
- typeof configBaseUrl === 'function' ? configBaseUrl() : configBaseUrl
378
- const path =
379
- typeof endpoint.path === 'function' ? endpoint.path(baseUrl) : endpoint.path
380
- const finalUrl = replacePathParams(`${baseUrl}${path}`, params)
381
- const queryString = new URLSearchParams(
382
- buildQueryParams(endpoint, reqQuery)
383
- ).toString()
384
-
385
- return {
386
- finalUrl,
387
- fullUrl: queryString ? `${finalUrl}?${queryString}` : finalUrl
388
- }
389
- }
390
-
391
- function buildQueryParams(
392
- endpoint: ApiEndpoint,
393
- query: QueryParams | undefined
394
- ): Record<string, string> {
395
- const defaultParams = endpoint.reqDefaultQueryParams ?? {}
396
- const parsedQuery = endpoint.reqQuerySchema
397
- ? (endpoint.reqQuerySchema.parse(query) as QueryParams)
398
- : (query ?? {})
399
- const merged = { ...defaultParams, ...parsedQuery }
400
-
401
- return Object.fromEntries(
402
- Object.entries(merged)
403
- .filter(([, value]) => value !== undefined)
404
- .map(([key, value]) => [key, String(value)])
405
- )
406
- }
407
-
408
- function buildRequestBody(endpoint: ApiEndpoint, body: unknown): unknown {
409
- if (!endpoint.reqBodySchema) return body
410
-
411
- const parsedBody = endpoint.reqBodySchema.parse(body) as Record<
412
- string,
413
- unknown
414
- >
415
-
416
- if (endpoint.reqBodyFormat !== 'form-data') {
417
- return parsedBody
418
- }
419
-
420
- const formData = new FormData()
421
- for (const [key, value] of Object.entries(parsedBody)) {
422
- if (value == null) continue
423
- formData.set(key, value instanceof Blob ? value : String(value))
424
- }
425
-
426
- return formData
427
- }
428
-
429
- function buildHeaders(
430
- endpoint: ApiEndpoint,
431
- headers: Record<string, string> | undefined,
432
- defaultHeaders: Record<string, string>
433
- ): Record<string, string> {
434
- const contentType =
435
- endpoint.reqBodyFormat === 'json' ? 'application/json' : undefined
436
- const parsedHeaders = endpoint.reqHeadersSchema
437
- ? (endpoint.reqHeadersSchema.parse(headers) as Record<string, string>)
438
- : (headers ?? {})
439
-
440
- return {
441
- ...defaultHeaders,
442
- ...(contentType && { 'Content-Type': contentType }),
443
- ...endpoint.defaultHeaders,
444
- ...parsedHeaders
445
- }
446
- }
447
-
448
- function buildFetchBody(
449
- body: unknown,
450
- headers: Record<string, string>
451
- ): RequestInit['body'] {
452
- if (body === undefined || body === null) return undefined
453
- if (body instanceof FormData || body instanceof Blob) return body
454
- if (headers['Content-Type'] === 'application/json')
455
- return JSON.stringify(body)
456
- return body as RequestInit['body']
457
- }
458
-
459
- function replacePathParams(
460
- path: string,
461
- params: PathParams | undefined
462
- ): string {
463
- return path.replace(/:(\w+)/g, (_, key: string) => {
464
- const value = params?.[key]
465
- if (value === undefined) {
466
- throw new Error(`Missing required path parameter: ${key}`)
467
- }
468
- return encodeURIComponent(String(value))
469
- })
470
- }
471
-
472
- function getHeadersAsObject(headers: Headers): Record<string, string> {
473
- const result: Record<string, string> = {}
474
- headers.forEach((value, key) => {
475
- result[key] = value
476
- })
477
- return result
478
- }
479
-
480
- function calculateSizeInMb(data: unknown): number {
481
- const sizeInBytes = new TextEncoder().encode(JSON.stringify(data)).length
482
- return sizeInBytes / (1024 * 1024)
483
- }
484
-
485
- function defaultShouldRetry({ error, response }: RetryContext): boolean {
486
- if (error) return true
487
- return response ? response.status >= 500 : false
488
- }
489
-
490
- function delay(ms: number): Promise<void> {
491
- return new Promise((resolve) => setTimeout(resolve, ms))
492
- }
1
+ export type { FormattedZodIssue } from './zod-issues'
2
+ export * from './types'
3
+ export * from './errors'
4
+ export { createApiClient } from './create-client'
5
+ export { defineAction } from './actions-impl'
@@ -0,0 +1,62 @@
1
+ import type { ApiErrorContext } from './shared'
2
+
3
+ const SENSITIVE_HEADER_KEYS = new Set([
4
+ 'authorization',
5
+ 'cookie',
6
+ 'set-cookie',
7
+ 'x-api-key',
8
+ 'x-auth-token'
9
+ ])
10
+
11
+ const SENSITIVE_BODY_KEYS = new Set([
12
+ 'password',
13
+ 'token',
14
+ 'accesstoken',
15
+ 'access_token',
16
+ 'refreshtoken',
17
+ 'refresh_token',
18
+ 'secret',
19
+ 'client_secret',
20
+ 'clientsecret',
21
+ 'apikey',
22
+ 'api_key'
23
+ ])
24
+
25
+ function redactHeaders(
26
+ headers: Record<string, string> | undefined
27
+ ): Record<string, string> | undefined {
28
+ if (!headers) return undefined
29
+ const out: Record<string, string> = {}
30
+ for (const [k, v] of Object.entries(headers)) {
31
+ if (SENSITIVE_HEADER_KEYS.has(k.toLowerCase())) {
32
+ out[k] = '[REDACTED]'
33
+ } else {
34
+ out[k] = v
35
+ }
36
+ }
37
+ return out
38
+ }
39
+
40
+ function redactBody(body: unknown): unknown {
41
+ if (body === null || body === undefined) return body
42
+ if (typeof body !== 'object' || Array.isArray(body)) {
43
+ return body
44
+ }
45
+ const rec = body as Record<string, unknown>
46
+ const out: Record<string, unknown> = { ...rec }
47
+ for (const key of Object.keys(out)) {
48
+ if (SENSITIVE_BODY_KEYS.has(key.toLowerCase())) {
49
+ out[key] = '[REDACTED]'
50
+ }
51
+ }
52
+ return out
53
+ }
54
+
55
+ export function defaultRedactContext(ctx: ApiErrorContext): ApiErrorContext {
56
+ return {
57
+ ...ctx,
58
+ requestHeaders: redactHeaders(ctx.requestHeaders),
59
+ requestBody: redactBody(ctx.requestBody),
60
+ meta: ctx.meta
61
+ }
62
+ }
@@ -0,0 +1,25 @@
1
+ export type HttpMethod =
2
+ | 'GET'
3
+ | 'POST'
4
+ | 'PUT'
5
+ | 'DELETE'
6
+ | 'PATCH'
7
+ | 'HEAD'
8
+ | 'OPTIONS'
9
+
10
+ export type RequestBodyFormat = 'json' | 'form-data'
11
+ export type BaseApiUrl = string | (() => string)
12
+ export type PathParams = Record<string, string | number>
13
+ export type QueryParams = Record<string, string | number | boolean | undefined>
14
+
15
+ export type ApiErrorContext = {
16
+ endpoint: string
17
+ method: HttpMethod
18
+ url: string
19
+ attempt: number
20
+ maxRetries: number
21
+ elapsedMs: number
22
+ requestHeaders?: Record<string, string>
23
+ requestBody?: unknown
24
+ meta?: Record<string, unknown>
25
+ }
@@ -0,0 +1,19 @@
1
+ export function calculateSizeInMb(data: unknown): number {
2
+ let bytes: number
3
+ if (data === undefined || data === null) {
4
+ bytes = 0
5
+ } else if (data instanceof Blob) {
6
+ bytes = data.size
7
+ } else if (typeof FormData !== 'undefined' && data instanceof FormData) {
8
+ bytes = 0
9
+ data.forEach((value) => {
10
+ if (value instanceof Blob) bytes += value.size
11
+ else bytes += new TextEncoder().encode(String(value)).length
12
+ })
13
+ } else if (typeof data === 'string') {
14
+ bytes = new TextEncoder().encode(data).length
15
+ } else {
16
+ bytes = new TextEncoder().encode(JSON.stringify(data) ?? '').length
17
+ }
18
+ return bytes / (1024 * 1024)
19
+ }
@@ -0,0 +1,25 @@
1
+ import { mock } from 'bun:test'
2
+
3
+ export const originalFetch = globalThis.fetch
4
+
5
+ export const createMockResponse = (
6
+ body: unknown,
7
+ options: { status?: number; contentType?: string } = {}
8
+ ) => {
9
+ const { status = 200, contentType = 'application/json' } = options
10
+ return new Response(
11
+ contentType.includes('application/json')
12
+ ? JSON.stringify(body)
13
+ : String(body),
14
+ {
15
+ status,
16
+ headers: { 'content-type': contentType }
17
+ }
18
+ )
19
+ }
20
+
21
+ export const mockFetch = (
22
+ fn: (url: string, options?: RequestInit) => Promise<Response>
23
+ ) => {
24
+ globalThis.fetch = mock(fn) as unknown as typeof fetch
25
+ }