create-request 1.6.1 → 2.0.0-next.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.
@@ -0,0 +1,851 @@
1
+ //#region src/schema.d.ts
2
+ /**
3
+ * The Standard Schema interface (https://standardschema.dev), version 1.
4
+ *
5
+ * Any schema library that implements it — zod (≥ 3.24), valibot (≥ 1.0), arktype (≥ 2.0),
6
+ * effect/Schema and others — can be passed to `getJson(schema)`, `getData(schema)` and
7
+ * `getResult(schema)` to validate (and type) the response body without adding a dependency
8
+ * on any particular library.
9
+ */
10
+ interface StandardSchemaV1<Input = unknown, Output = Input> {
11
+ /** The Standard Schema properties. */
12
+ readonly "~standard": StandardSchemaV1.Props<Input, Output>;
13
+ }
14
+ declare namespace StandardSchemaV1 {
15
+ /** The Standard Schema properties interface. */
16
+ interface Props<Input = unknown, Output = Input> {
17
+ /** The version number of the standard. */
18
+ readonly version: 1;
19
+ /** The vendor name of the schema library. */
20
+ readonly vendor: string;
21
+ /** Validates unknown input values. */
22
+ readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
23
+ /** Inferred types associated with the schema. */
24
+ readonly types?: Types<Input, Output> | undefined;
25
+ }
26
+ /** The result interface of the validate function. */
27
+ type Result<Output> = SuccessResult<Output> | FailureResult;
28
+ /** The result interface if validation succeeds. */
29
+ interface SuccessResult<Output> {
30
+ /** The typed output value. */
31
+ readonly value: Output;
32
+ /** The non-existent issues. */
33
+ readonly issues?: undefined;
34
+ }
35
+ /** The result interface if validation fails. */
36
+ interface FailureResult {
37
+ /** The issues of failed validation. */
38
+ readonly issues: readonly Issue[];
39
+ }
40
+ /** The issue interface of the failure output. */
41
+ interface Issue {
42
+ /** The error message of the issue. */
43
+ readonly message: string;
44
+ /** The path of the issue, if any. */
45
+ readonly path?: readonly (PropertyKey | PathSegment)[] | undefined;
46
+ }
47
+ /** The path segment interface of the issue. */
48
+ interface PathSegment {
49
+ /** The key representing a path segment. */
50
+ readonly key: PropertyKey;
51
+ }
52
+ /** The Standard Schema types interface. */
53
+ interface Types<Input = unknown, Output = Input> {
54
+ /** The input type of the schema. */
55
+ readonly input: Input;
56
+ /** The output type of the schema. */
57
+ readonly output: Output;
58
+ }
59
+ /** Infers the input type of a Standard Schema. */
60
+ type InferInput<Schema extends StandardSchemaV1> = NonNullable<Schema["~standard"]["types"]>["input"];
61
+ /** Infers the output type of a Standard Schema. */
62
+ type InferOutput<Schema extends StandardSchemaV1> = NonNullable<Schema["~standard"]["types"]>["output"];
63
+ }
64
+ //#endregion
65
+ //#region src/error.d.ts
66
+ /** Everything a {@link RequestError} can carry besides its message. */
67
+ interface RequestErrorOptions {
68
+ /** What kind of failure this is — see {@link RequestErrorCode}. */
69
+ code: RequestErrorCode;
70
+ /** The URL that was requested (with query string). */
71
+ url: string;
72
+ /** The HTTP method that was used. */
73
+ method: Method;
74
+ /** HTTP status, when a response was received. */
75
+ status?: number | undefined;
76
+ /** The raw `Response`, when one was received. Its body has been read into `body` (unless it was larger than 1 MB). */
77
+ response?: Response | undefined;
78
+ /** The response body as text, when one was received and could be read (at most 1 MB). */
79
+ body?: string | undefined;
80
+ /** Schema issues, for `"VALIDATION"` errors coming from `getJson(schema)` and friends. */
81
+ issues?: readonly StandardSchemaV1.Issue[] | undefined;
82
+ /** The underlying error (what `fetch`, an interceptor or `JSON.parse` threw). */
83
+ cause?: unknown;
84
+ }
85
+ /**
86
+ * The single error type thrown by this library. Every rejection from `getResponse()`, `getJson()`, …
87
+ * is a `RequestError`, so one `instanceof` (or {@link isRequestError}) check is enough, and `code`
88
+ * tells you what happened.
89
+ *
90
+ * @example
91
+ * ```typescript
92
+ * try {
93
+ * await api.get("/users/42").getJson<User>();
94
+ * } catch (error) {
95
+ * if (!isRequestError(error)) throw error;
96
+ * switch (error.code) {
97
+ * case "HTTP": console.log(error.status, error.data); break; // parsed error body
98
+ * case "TIMEOUT": console.log("try again later"); break;
99
+ * case "ABORTED": break; // the user navigated away
100
+ * default: console.error(error.message, error.cause);
101
+ * }
102
+ * }
103
+ * ```
104
+ */
105
+ export declare class RequestError<TData = unknown> extends Error {
106
+ readonly name = "RequestError";
107
+ /** What kind of failure this is — see {@link RequestErrorCode}. */
108
+ readonly code: RequestErrorCode;
109
+ /** The URL that was requested (with query string). */
110
+ readonly url: string;
111
+ /** The HTTP method that was used. */
112
+ readonly method: Method;
113
+ /** HTTP status, when a response was received. */
114
+ readonly status?: number | undefined;
115
+ /** The raw `Response`, when one was received. Its body has been read into `body` (unless it was larger than 1 MB). */
116
+ readonly response?: Response | undefined;
117
+ /** The response body as text, when one was received and could be read (at most 1 MB). */
118
+ readonly body?: string | undefined;
119
+ /** Schema issues, for `"VALIDATION"` errors coming from `getJson(schema)` and friends. */
120
+ readonly issues?: readonly StandardSchemaV1.Issue[] | undefined;
121
+ private _data?;
122
+ constructor(message: string, options: RequestErrorOptions);
123
+ /**
124
+ * `body` parsed as JSON — the shape most APIs use for error details.
125
+ * `undefined` when there is no body or it is not valid JSON; never throws.
126
+ *
127
+ * @example
128
+ * ```typescript
129
+ * catch (error) {
130
+ * if (isRequestError(error)) console.log(error.data?.message ?? error.message);
131
+ * }
132
+ * ```
133
+ */
134
+ get data(): TData | undefined;
135
+ /** Shorthand for `code === "TIMEOUT"`. */
136
+ get isTimeout(): boolean;
137
+ /** Shorthand for `code === "ABORTED"`. */
138
+ get isAborted(): boolean;
139
+ }
140
+ /** Type guard for {@link RequestError} — handy in `catch (error: unknown)` blocks. */
141
+ export declare const isRequestError: (error: unknown) => error is RequestError;
142
+ //#endregion
143
+ //#region src/types.d.ts
144
+ /** The HTTP methods a request can be created with. */
145
+ type Method = "GET" | "HEAD" | "OPTIONS" | "DELETE" | "POST" | "PUT" | "PATCH";
146
+ /** The HTTP methods that may carry a request body (`withBody` / `withGraphQL` are only available on these). */
147
+ type BodyMethod = "POST" | "PUT" | "PATCH" | "DELETE";
148
+ /** What `fetch` accepts as a body. */
149
+ type FetchBody = NonNullable<RequestInit["body"]>;
150
+ /** `"include"`, `"omit"` or `"same-origin"`. */
151
+ type CredentialsMode = NonNullable<RequestInit["credentials"]>;
152
+ /** `"cors"`, `"no-cors"`, `"same-origin"` or `"navigate"`. */
153
+ type CorsMode = NonNullable<RequestInit["mode"]>;
154
+ /** `"follow"`, `"error"` or `"manual"`. */
155
+ type RedirectMode = NonNullable<RequestInit["redirect"]>;
156
+ /** A referrer policy name (`"no-referrer"`, `"strict-origin-when-cross-origin"`, …). */
157
+ type ReferrerPolicyName = NonNullable<RequestInit["referrerPolicy"]>;
158
+ /** How the HTTP cache is used. */
159
+ type CacheMode = "default" | "force-cache" | "no-cache" | "no-store" | "only-if-cached" | "reload";
160
+ /** A fetch priority hint. */
161
+ type PriorityHint = "auto" | "high" | "low";
162
+ /**
163
+ * Anything `withBody()` accepts.
164
+ *
165
+ * - `string` → sent as-is (`Content-Type: text/plain` unless you set one)
166
+ * - `Blob` / `File`, `FormData`, `URLSearchParams` → sent as-is, `fetch` sets the matching `Content-Type`
167
+ * - `ArrayBuffer`, typed arrays, `ReadableStream` → sent as-is, no `Content-Type` unless you set one
168
+ * - any other JSON-serialisable object or array → `JSON.stringify`-ed (`Content-Type: application/json` unless you set one)
169
+ *
170
+ * @example
171
+ * ```typescript
172
+ * request.withBody({ name: "Ada" }); // JSON
173
+ * request.withBody(new FormData(form)); // multipart
174
+ * request.withBody("id=1&name=Ada"); // text/plain — set withContentType() for form-urlencoded
175
+ * ```
176
+ */
177
+ type Body = FetchBody | object;
178
+ /** A single query-string value. Arrays produce repeated keys (`?tag=a&tag=b`); `null`/`undefined` remove the key. */
179
+ type QueryValue = string | number | boolean | Date | null | undefined | readonly (string | number | boolean | Date)[];
180
+ /** Query parameters as a plain object (`{ page: 1, tags: ["a", "b"] }`) or as `URLSearchParams`. */
181
+ type QueryParams = Record<string, QueryValue> | URLSearchParams;
182
+ /** Headers as a plain object. `null`/`undefined` unsets the header (useful to drop an api-level default). */
183
+ type HeadersRecord = Record<string, string | number | null | undefined>;
184
+ /** Cookies as a plain object of `name → value`. Values are sent verbatim (no encoding). */
185
+ type CookiesRecord = Record<string, string>;
186
+ /** A fetch-compatible function: receives the final URL and `RequestInit` and returns a `Response`. */
187
+ type FetchFunction = (url: string, init: RequestInit) => Promise<Response>;
188
+ /**
189
+ * Discriminates the kind of failure a {@link RequestError} represents.
190
+ *
191
+ * - `"HTTP"` — the server answered with a non-2xx status (`status`, `response`, `body`, `data` are set)
192
+ * - `"NETWORK"` — `fetch` itself rejected: DNS, connection refused, CORS, offline, a relative URL in Node.js
193
+ * (`cause` is the original error)
194
+ * - `"TIMEOUT"` — the `withTimeout()` deadline passed, or a signal aborted with a `TimeoutError` (`AbortSignal.timeout()`)
195
+ * - `"ABORTED"` — a signal passed with `withSignal()` / `withAbortController()` was aborted
196
+ * - `"PARSE"` — the body could not be read or parsed (invalid JSON, body already consumed, selector threw)
197
+ * - `"VALIDATION"` — the response did not match the schema passed to `getJson(schema)` (`issues` is set),
198
+ * or the request could not be built: an empty or unparsable absolute URL, an invalid header, timeout,
199
+ * retries or body (those last three are thrown synchronously by the `with*` method)
200
+ * - `"INTERCEPTOR"` — an interceptor or callback (retry, CSRF token) threw (`cause` is what it threw)
201
+ * - `"GRAPHQL"` — the GraphQL response contained `errors` and `throwOnError` was enabled
202
+ */
203
+ type RequestErrorCode = "HTTP" | "NETWORK" | "TIMEOUT" | "ABORTED" | "PARSE" | "VALIDATION" | "INTERCEPTOR" | "GRAPHQL";
204
+ /** What a retry decision or delay function receives. */
205
+ interface RetryContext {
206
+ /** The retry about to happen, starting at 1 (so `attempt === 1` follows the first failure). */
207
+ attempt: number;
208
+ /** The error that triggered this retry. */
209
+ error: RequestError;
210
+ }
211
+ /**
212
+ * Configuration for automatic retries — see `withRetries()`.
213
+ *
214
+ * By default a request is retried after a network error, a timeout, or a response with status
215
+ * 408, 425, 429, 500, 502, 503 or 504, with exponential backoff (300 ms, 600 ms, 1.2 s, … plus
216
+ * up to 100 ms of jitter, capped at `maxDelay`). A `Retry-After` header is honoured when present.
217
+ * Aborted requests and requests with a `ReadableStream` body are never retried, whatever the policy.
218
+ *
219
+ * @example
220
+ * ```typescript
221
+ * request.withRetries(3); // 3 retries, default policy
222
+ * request.withRetries({ attempts: 3, delay: 1000 }); // fixed 1 s delay
223
+ * request.withRetries({ attempts: 5, methods: ["GET", "HEAD"] }); // idempotent methods only
224
+ * request.withRetries({
225
+ * attempts: 3,
226
+ * delay: ({ attempt }) => attempt * 500,
227
+ * shouldRetry: ({ error }) => error.code === "HTTP" && error.status === 503,
228
+ * onRetry: ({ attempt, delay }) => console.log(`retry #${attempt} in ${delay}ms`),
229
+ * });
230
+ * ```
231
+ */
232
+ interface RetryConfig {
233
+ /** Number of retries after the first attempt (`3` means up to 4 requests in total). */
234
+ attempts: number;
235
+ /**
236
+ * Delay before each retry, in milliseconds, or a function computing it.
237
+ * When set, it takes precedence over a `Retry-After` header. Default: exponential backoff.
238
+ */
239
+ delay?: number | ((context: RetryContext) => number) | undefined;
240
+ /** Statuses that are retried. Default: `[408, 425, 429, 500, 502, 503, 504]`. Network errors and timeouts are retried by default. */
241
+ statuses?: readonly number[] | undefined;
242
+ /** Methods that are retried. Default: all. Use `["GET", "HEAD", "OPTIONS", "PUT", "DELETE"]` to retry idempotent requests only. */
243
+ methods?: readonly Method[] | undefined;
244
+ /**
245
+ * Upper bound, in milliseconds, for the default backoff. A `Retry-After` header longer than this
246
+ * cancels the retry so you can react to it yourself. Default: `30000`.
247
+ */
248
+ maxDelay?: number | undefined;
249
+ /** Full override of the retry decision (`statuses` and `methods` are ignored). Aborts and stream bodies still never retry. */
250
+ shouldRetry?: ((context: RetryContext) => boolean | Promise<boolean>) | undefined;
251
+ /** Called before every retry, after the delay has been computed. Awaited if it returns a promise. */
252
+ onRetry?: ((context: RetryContext & {
253
+ delay: number;
254
+ }) => void | Promise<void>) | undefined;
255
+ }
256
+ /**
257
+ * The request as it is about to be sent, handed to request interceptors.
258
+ *
259
+ * `headers` keys are lower-case; `body` is already serialised (JSON bodies are strings);
260
+ * `signal` combines the signals passed with `withSignal()` (the timeout is added after interceptors ran).
261
+ * Mutate it in place or return a new object; return a `Response` to skip the network entirely.
262
+ */
263
+ interface RequestConfig extends Omit<RequestInit, "headers" | "body" | "method" | "signal" | "window"> {
264
+ /** The final URL, with the query string and, for api requests, the base URL applied. */
265
+ url: string;
266
+ /** The HTTP method. */
267
+ method: Method;
268
+ /** The headers to send, with lower-case names. The CSRF header (`withCsrf()`) is added after interceptors ran. */
269
+ headers: Record<string, string>;
270
+ /** The serialised body: JSON bodies are already strings. */
271
+ body?: FetchBody | null | undefined;
272
+ /** The signals passed with `withSignal()` / `withAbortController()`, combined. The timeout is added after interceptors ran. */
273
+ signal?: AbortSignal | undefined;
274
+ /** Required by browsers and Node for `ReadableStream` bodies; set automatically. */
275
+ duplex?: "half";
276
+ }
277
+ /**
278
+ * Runs before the request is sent. Return nothing to keep your in-place changes, a new
279
+ * {@link RequestConfig} to replace it, or a `Response` to short-circuit the request
280
+ * (it is then treated like a fetched response: status is checked, response interceptors run —
281
+ * but `withTimeout()` does not apply to it, since nothing was fetched).
282
+ *
283
+ * @example
284
+ * ```typescript
285
+ * const withTraceId: RequestInterceptor = config => {
286
+ * config.headers["x-trace-id"] = crypto.randomUUID();
287
+ * };
288
+ * ```
289
+ */
290
+ type RequestInterceptor = (config: RequestConfig) => RequestConfig | Response | void | Promise<RequestConfig | Response | void>;
291
+ /**
292
+ * Runs after a successful response (before the body is read), with the request that produced it.
293
+ * Return nothing to keep the response or another {@link ResponseWrapper} to replace it.
294
+ *
295
+ * @example
296
+ * ```typescript
297
+ * const logStatus: ResponseInterceptor = (response, request) => {
298
+ * console.log(request.method, response.url, response.status);
299
+ * };
300
+ * ```
301
+ */
302
+ type ResponseInterceptor = (response: ResponseWrapper, request: HttpRequest) => ResponseWrapper | void | Promise<ResponseWrapper | void>;
303
+ /**
304
+ * Runs once when the request has failed for good (after all retries), with the request that failed.
305
+ * Return nothing to keep the error, another {@link RequestError} to replace it, or a
306
+ * {@link ResponseWrapper} to recover — typically by replaying `request.clone()`. Throwing replaces the error as well.
307
+ *
308
+ * @example
309
+ * ```typescript
310
+ * const replayed = new WeakSet<HttpRequest>();
311
+ * const refreshOn401: ErrorInterceptor = async (error, request) => {
312
+ * if (error.status !== 401 || replayed.has(request)) return;
313
+ * token = await refreshToken(); // the request interceptor that adds the token reads it
314
+ * const retry = request.clone();
315
+ * replayed.add(retry); // never loop on a persistent 401
316
+ * return retry.getResponse();
317
+ * };
318
+ * ```
319
+ */
320
+ type ErrorInterceptor = (error: RequestError, request: HttpRequest) => RequestError | ResponseWrapper | void | Promise<RequestError | ResponseWrapper | void>;
321
+ /** Options for `withGraphQL()`. */
322
+ interface GraphQLOptions {
323
+ /** Throw a `RequestError` with code `"GRAPHQL"` when the response contains a non-empty `errors` array. Default: `false`. */
324
+ throwOnError?: boolean | undefined;
325
+ }
326
+ /**
327
+ * Options for `withCsrf()`.
328
+ *
329
+ * The token is attached only to same-origin requests (evaluated against the final URL, after
330
+ * interceptors, before any redirect) unless `crossOrigin` is set. Outside a browser there is no
331
+ * page origin and every request counts as same-origin.
332
+ */
333
+ interface CsrfOptions {
334
+ /** Cookie to read the token from (browser only). Default: `"XSRF-TOKEN"`. Ignored when `token` is set. */
335
+ cookie?: string | undefined;
336
+ /** Header the token is sent in. Default: `"X-XSRF-TOKEN"` when read from a cookie, `"X-CSRF-Token"` when `token` is set. */
337
+ header?: string | undefined;
338
+ /** A token, or a function returning one per request (return `null`/`undefined` to send nothing). */
339
+ token?: string | (() => string | null | undefined) | undefined;
340
+ /** Also attach the token to cross-origin URLs. Default: `false`. */
341
+ crossOrigin?: boolean | undefined;
342
+ }
343
+ /** What `getResult()` resolves to: `error` is `null` on success and the {@link RequestError} otherwise (then `data` is `null`). */
344
+ type RequestResult<T> = {
345
+ data: T;
346
+ error: null;
347
+ } | {
348
+ data: null;
349
+ error: RequestError;
350
+ };
351
+ //#endregion
352
+ //#region src/response.d.ts
353
+ /**
354
+ * A successful response. Exposes the status line and headers, and reads the body in the format you
355
+ * ask for. The body is buffered once, so you can call several readers, in any order and concurrently
356
+ * (`getJson()` after `getText()`, `Promise.all([res.getJson(), res.getBlob()])`, …) — except
357
+ * `getBody()`, which hands you the live stream and therefore excludes the others.
358
+ *
359
+ * `T` is the JSON type declared at the request (`api.get<User>()`) and is the default for `getJson()` / `getData()`.
360
+ *
361
+ * @example
362
+ * ```typescript
363
+ * const res = await api.get<User>("/me").getResponse();
364
+ * console.log(res.status, res.headers.get("etag"));
365
+ * const user = await res.getJson(); // User
366
+ * ```
367
+ */
368
+ export declare class ResponseWrapper<T = unknown> {
369
+ /** The underlying `Response`. Read its body through this wrapper's methods, or take it over with `getBody()`. */
370
+ readonly raw: Response;
371
+ /** The URL that was requested, including the query string (after interceptors). */
372
+ readonly url: string;
373
+ /** The HTTP method that was used. */
374
+ readonly method: Method;
375
+ private _bin?;
376
+ private _text?;
377
+ private _json?;
378
+ /** Wraps a `Response` — useful to hand a synthetic response to an error interceptor. */
379
+ constructor(
380
+ /** The underlying `Response`. Read its body through this wrapper's methods, or take it over with `getBody()`. */
381
+ raw: Response,
382
+ /** The URL that was requested, including the query string (after interceptors). */
383
+ url?: string,
384
+ /** The HTTP method that was used. */
385
+ method?: Method);
386
+ /** HTTP status code (`200`, `404`, …). */
387
+ get status(): number;
388
+ /** HTTP status text (`"OK"`, `"Not Found"`, …); empty over HTTP/2. */
389
+ get statusText(): string;
390
+ /** Whether the status is in the 200–299 range. */
391
+ get ok(): boolean;
392
+ /** Response headers. */
393
+ get headers(): Headers;
394
+ private _err;
395
+ /** The error for a body that cannot be read: TIMEOUT / ABORTED when the deadline or a signal fired, otherwise PARSE with `fallback`. */
396
+ private _unreadable;
397
+ private _buffer;
398
+ private _validate;
399
+ /** The body as an `ArrayBuffer`. */
400
+ getArrayBuffer(): Promise<ArrayBuffer>;
401
+ /** The body as a `Blob`, typed with the response's `Content-Type`. */
402
+ getBlob(): Promise<Blob>;
403
+ /** The body decoded as UTF-8 text (`""` for an empty body). */
404
+ getText(): Promise<string>;
405
+ /** The body parsed as `multipart/form-data` or `application/x-www-form-urlencoded`. */
406
+ getFormData(): Promise<FormData>;
407
+ /**
408
+ * The raw body stream, for streaming consumption (downloads with progress, SSE/NDJSON, LLM output…).
409
+ * Unlike the other readers it is not buffered: the body can be read once, and only if no other reader ran.
410
+ * Taking the stream also ends the request's `withTimeout()` deadline — from here on the stream is yours.
411
+ *
412
+ * @example
413
+ * ```typescript
414
+ * const reader = (await request.getBody())!.pipeThrough(new TextDecoderStream()).getReader();
415
+ * for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) process(chunk.value);
416
+ * ```
417
+ */
418
+ getBody(): ReadableStream<Uint8Array> | null;
419
+ /**
420
+ * The body parsed as JSON. An empty body (`204`, `Content-Length: 0`, whitespace) yields `null` —
421
+ * declare it in the type (`getJson<User | null>()`) for endpoints that may return nothing.
422
+ *
423
+ * Pass a [Standard Schema](https://standardschema.dev) (zod, valibot, arktype, …) to validate the body
424
+ * and infer its type; a mismatch rejects with a `RequestError` whose `code` is `"VALIDATION"` and
425
+ * whose `issues` lists what went wrong.
426
+ *
427
+ * @example
428
+ * ```typescript
429
+ * const user = await res.getJson<User>();
430
+ * const user = await res.getJson(UserSchema); // typed from the schema, validated at runtime
431
+ * ```
432
+ */
433
+ getJson<U = T>(): Promise<U>;
434
+ getJson<S extends StandardSchemaV1>(schema: S): Promise<StandardSchemaV1.InferOutput<S>>;
435
+ /**
436
+ * `getJson()` followed by a selector, so you can pick the part you need in one call. The selector
437
+ * receives `T` (declare it on the request: `api.get<Page>()`), or pass both type arguments explicitly.
438
+ * Errors thrown by the selector are reported as a `RequestError` with code `"PARSE"`. A schema can be validated first.
439
+ *
440
+ * @example
441
+ * ```typescript
442
+ * const names = await res.getData(page => page.users.map(u => u.name)); // res: ResponseWrapper<Page>
443
+ * const names = await res.getData<Page, string[]>(page => page.users.map(u => u.name));
444
+ * const names = await res.getData(PageSchema, page => page.users.map(u => u.name));
445
+ * ```
446
+ */
447
+ getData<U = T>(): Promise<U>;
448
+ getData<S extends StandardSchemaV1>(schema: S): Promise<StandardSchemaV1.InferOutput<S>>;
449
+ getData<S extends StandardSchemaV1, R>(schema: S, selector: (data: StandardSchemaV1.InferOutput<S>) => R): Promise<R>;
450
+ getData<R>(selector: (data: T) => R): Promise<R>;
451
+ getData<U, R>(selector: (data: U) => R): Promise<R>;
452
+ }
453
+ //#endregion
454
+ //#region src/request.d.ts
455
+ /**
456
+ * A request under construction. Configure it with the `with*` methods (each returns the same request,
457
+ * so calls chain), then execute it with one of the `get*` methods. Nothing is sent until you execute.
458
+ *
459
+ * `M` is the HTTP method — `withBody()` and `withGraphQL()` only exist for methods that carry a body —
460
+ * and `T` is the JSON type the response is expected to have (`api.get<User>("/me")`), used as the
461
+ * default for `getJson()` / `getData()` / `getResult()`.
462
+ *
463
+ * Create requests with `create.get(url)`, `create.post(url)`, … or through an api instance; the class
464
+ * is exported for type annotations and `instanceof` checks.
465
+ *
466
+ * @example
467
+ * ```typescript
468
+ * const user = await create
469
+ * .post("https://api.example.com/users")
470
+ * .withBearerToken(token)
471
+ * .withBody({ name: "Ada" })
472
+ * .withTimeout(5000)
473
+ * .withRetries(2)
474
+ * .getJson<User>();
475
+ * ```
476
+ */
477
+ export declare class HttpRequest<M extends Method = Method, T = unknown> {
478
+ /** The HTTP method. */
479
+ readonly method: M;
480
+ private readonly _url;
481
+ /** Prefer `create.get(url)`, `create.post(url)`, … or an api instance to construct requests. */
482
+ constructor(
483
+ /** The HTTP method. */
484
+ method: M, url: string);
485
+ /** The URL this request will be sent to, including the query string. */
486
+ get url(): string;
487
+ private _fail;
488
+ private _set;
489
+ /**
490
+ * Sets several headers at once. Names are case-insensitive (stored lower-case); a `null` or `undefined`
491
+ * value removes the header, which is how a request drops a default set on its api.
492
+ *
493
+ * @example
494
+ * ```typescript
495
+ * request.withHeaders({ Accept: "application/json", "X-Request-Id": id });
496
+ * api.get("/public").withHeaders({ Authorization: null }); // send this one unauthenticated
497
+ * ```
498
+ */
499
+ withHeaders<H extends { [K in keyof H]: string | number | null | undefined; }>(headers: H): this;
500
+ /** Sets one header — see {@link withHeaders}. */
501
+ withHeader(name: string, value: string | number | null | undefined): this;
502
+ /**
503
+ * Sets the `Content-Type` header. Rarely needed: JSON and text bodies set it automatically and
504
+ * `FormData` / `Blob` bodies must leave it to `fetch` (it adds the multipart boundary).
505
+ */
506
+ withContentType(contentType: string): this;
507
+ /** Sets the `Authorization` header verbatim (`"Bearer …"`, `"Basic …"`, a custom scheme). */
508
+ withAuthorization(value: string): this;
509
+ /** Sets `Authorization: Bearer <token>`. */
510
+ withBearerToken(token: string): this;
511
+ /** Sets `Authorization: Basic <base64(username:password)>` (UTF-8 safe). */
512
+ withBasicAuth(username: string, password: string): this;
513
+ /**
514
+ * Adds cookies to the `Cookie` header. Names and values are sent verbatim — encode them yourself if
515
+ * needed, and never pass untrusted values (a `;` would smuggle in another cookie). **Node.js /
516
+ * server-side only**: browsers ignore a `Cookie` header on `fetch` and send their own cookies
517
+ * instead — use `withCredentials("include")` there.
518
+ */
519
+ withCookies<C extends { [K in keyof C]: string; }>(cookies: C): this;
520
+ /** Adds one cookie — see {@link withCookies}. */
521
+ withCookie(name: string, value: string): this;
522
+ /**
523
+ * Enables CSRF protection: a token is attached to requests whose URL (final, after interceptors) is
524
+ * same-origin with the page, so it is not sent to third-party hosts. By default the token is read
525
+ * from the `XSRF-TOKEN` cookie and sent as `X-XSRF-TOKEN` — the convention of Angular, Laravel and
526
+ * Spring. See {@link CsrfOptions} for other setups.
527
+ *
528
+ * Two limits of that guarantee: the check happens before `fetch` runs, so a same-origin URL that
529
+ * *redirects* to another origin still carries the header (`fetch` only strips `Authorization` on
530
+ * cross-origin redirects) — use `withRedirect("error")` for endpoints that may redirect elsewhere;
531
+ * and outside a browser there is no page origin, so every URL counts as same-origin.
532
+ *
533
+ * @example
534
+ * ```typescript
535
+ * api.withCsrf(); // XSRF-TOKEN cookie → X-XSRF-TOKEN
536
+ * api.withCsrf({ cookie: "csrftoken", header: "X-CSRFToken" }); // Django
537
+ * api.withCsrf({ token: () => document.querySelector("meta[name=csrf-token]")?.getAttribute("content") }); // Rails
538
+ * ```
539
+ */
540
+ withCsrf(options?: CsrfOptions): this;
541
+ /**
542
+ * Sends a CSRF token you already hold in the given header (default `X-CSRF-Token`) — with every request,
543
+ * whatever its origin. It is a plain header; prefer {@link withCsrf} (`{ token }`) to keep the same-origin check.
544
+ */
545
+ withCsrfToken(token: string, header?: string): this;
546
+ /**
547
+ * Sets query parameters. A key that is already present is replaced (so a request can override an
548
+ * api-level default); arrays produce repeated keys, `Date`s are sent as ISO strings, and
549
+ * `null` / `undefined` remove the key. Accepts a `URLSearchParams` too.
550
+ *
551
+ * @example
552
+ * ```typescript
553
+ * request.withQueryParams({ page: 2, tags: ["a", "b"], since: new Date() });
554
+ * // ?page=2&tags=a&tags=b&since=2026-01-01T00%3A00%3A00.000Z
555
+ * ```
556
+ */
557
+ withQueryParams<P extends { [K in keyof P]: QueryValue; }>(params: P | URLSearchParams): this;
558
+ /** Sets one query parameter — see {@link withQueryParams}. */
559
+ withQueryParam(key: string, value: QueryValue): this;
560
+ /**
561
+ * Fails the request with code `"TIMEOUT"` if it has not completed — response received *and* body read —
562
+ * within `ms` milliseconds. The clock starts after request interceptors ran and applies to each attempt
563
+ * when retries are enabled; `getBody()` ends the deadline and hands you the stream. It does not cover
564
+ * a `Response` returned by a request interceptor (nothing was fetched).
565
+ * `0`, `Infinity` or anything above 2^31 − 1 ms (≈ 24.8 days, the limit of `setTimeout`) remove a
566
+ * timeout set earlier (for instance by an api instance).
567
+ */
568
+ withTimeout(ms: number): this;
569
+ /**
570
+ * Retries failed requests. Pass a number of retries for the default policy (network errors, timeouts,
571
+ * 408/425/429/500/502/503/504, exponential backoff, `Retry-After` honoured) or a {@link RetryConfig}
572
+ * to tune it. Calling it again merges with the previous configuration.
573
+ */
574
+ withRetries(retries: number | RetryConfig): this;
575
+ /** Called before every retry — shorthand for `withRetries({ onRetry })`. It does not enable retries by itself. */
576
+ onRetry(callback: RetryConfig["onRetry"]): this;
577
+ /**
578
+ * Cancels the request when `signal` aborts (error code `"ABORTED"`). Call it several times to
579
+ * combine signals — for instance the one your data-fetching library hands you and your own.
580
+ *
581
+ * @example
582
+ * ```typescript
583
+ * useQuery({ queryKey: ["user", id], queryFn: ({ signal }) => api.get(`/users/${id}`).withSignal(signal).getJson<User>() });
584
+ * ```
585
+ */
586
+ withSignal(signal: AbortSignal): this;
587
+ /** Cancels the request when `controller.abort()` is called — shorthand for `withSignal(controller.signal)`. */
588
+ withAbortController(controller: AbortController): this;
589
+ /**
590
+ * Uses `fetchFn` instead of the global `fetch`: inject a stub in tests, an undici `Agent` or proxy in
591
+ * Node.js, or a framework's patched fetch (Next.js caching options). It receives the final URL and
592
+ * `RequestInit` and must honour `init.signal` for timeouts and cancellation to work.
593
+ *
594
+ * @example
595
+ * ```typescript
596
+ * request.withFetch(async () => new Response('{"ok":true}')); // test stub
597
+ * request.withFetch((url, init) => undiciFetch(url, { ...init, dispatcher: agent })); // undici agent
598
+ * request.withFetch((url, init) => fetch(url, { ...init, next: { revalidate: 60 } })); // Next.js
599
+ * ```
600
+ */
601
+ withFetch(fetchFn: FetchFunction): this;
602
+ /** Whether cookies and auth headers are sent cross-origin: `"include"`, `"omit"` or `"same-origin"` (browser default). */
603
+ withCredentials(credentials: CredentialsMode): this;
604
+ /** CORS mode: `"cors"` (default), `"no-cors"` (opaque response, status 0 — not treated as an error) or `"same-origin"`. */
605
+ withMode(mode: CorsMode): this;
606
+ /**
607
+ * Redirect handling: `"follow"` (default), `"error"` (rejects with a network error) or `"manual"`, which
608
+ * resolves with the redirect itself — an opaque response (status 0) in browsers, the actual 3xx in Node.js.
609
+ */
610
+ withRedirect(redirect: RedirectMode): this;
611
+ /** The `Referer` to send (browser): a URL, `""` to send none, or `"about:client"` for the default. */
612
+ withReferrer(referrer: string): this;
613
+ /** How much referrer information the browser includes (`"no-referrer"`, `"strict-origin-when-cross-origin"`, …). */
614
+ withReferrerPolicy(policy: ReferrerPolicyName): this;
615
+ /** Priority hint for the browser's scheduler: `"high"`, `"low"` or `"auto"`. */
616
+ withPriority(priority: PriorityHint): this;
617
+ /** Lets the request outlive the page (analytics beacons, ≤ 64 KB body). */
618
+ withKeepAlive(keepalive?: boolean): this;
619
+ /** Subresource-integrity hash the response must match, e.g. `"sha256-…"`. */
620
+ withIntegrity(integrity: string): this;
621
+ /** How the browser's HTTP cache is used: `"default"`, `"no-store"`, `"reload"`, `"no-cache"`, `"force-cache"` or `"only-if-cached"`. */
622
+ withCache(cache: CacheMode): this;
623
+ /** Adds a {@link RequestInterceptor}. Api-level interceptors run first, then request-level ones, in registration order. */
624
+ withRequestInterceptor(interceptor: RequestInterceptor): this;
625
+ /** Adds a {@link ResponseInterceptor}, run after every successful response in registration order (it also receives the request). */
626
+ withResponseInterceptor(interceptor: ResponseInterceptor): this;
627
+ /** Adds an {@link ErrorInterceptor}, run once after the request has failed for good (after retries); it also receives the request, so it can replay it. */
628
+ withErrorInterceptor(interceptor: ErrorInterceptor): this;
629
+ /**
630
+ * Sets the request body (POST, PUT, PATCH and DELETE only — a compile error elsewhere). Objects and
631
+ * arrays are JSON-encoded; strings, `Blob`, `FormData`, `URLSearchParams`, `ArrayBuffer`, typed arrays
632
+ * and `ReadableStream` are sent as-is. `Content-Type` is set to `application/json` / `text/plain`
633
+ * unless already present, and removed for `FormData` (fetch must add the multipart boundary itself).
634
+ * Stream bodies are sent with `duplex: "half"` (Chromium and Node.js only) and are never retried.
635
+ *
636
+ * @example
637
+ * ```typescript
638
+ * create.post(url).withBody({ name: "Ada" });
639
+ * create.put(url).withBody(new FormData(form));
640
+ * ```
641
+ */
642
+ withBody(this: HttpRequest<BodyMethod, T>, body: Body): this;
643
+ /**
644
+ * Sends a GraphQL operation as a JSON body (`{ query, variables }`). With `throwOnError`, a response
645
+ * whose `errors` array is non-empty rejects `getJson()` with a `RequestError` of code `"GRAPHQL"`.
646
+ *
647
+ * @example
648
+ * ```typescript
649
+ * const { user } = await create
650
+ * .post<{ data: { user: User } }>("/graphql")
651
+ * .withGraphQL("query ($id: ID!) { user(id: $id) { name } }", { id }, { throwOnError: true })
652
+ * .getData(r => r.data);
653
+ * ```
654
+ */
655
+ withGraphQL(this: HttpRequest<BodyMethod, T>, query: string, variables?: object, options?: GraphQLOptions): this;
656
+ /**
657
+ * An independent copy of this request, so a configured request can serve as a template.
658
+ * Interceptors, signals and body objects are shared by reference (a `ReadableStream` body can only be sent once).
659
+ *
660
+ * @example
661
+ * ```typescript
662
+ * const search = api.get("/search").withTimeout(2000);
663
+ * const page1 = search.clone().withQueryParam("page", 1).getJson();
664
+ * const page2 = search.clone().withQueryParam("page", 2).getJson();
665
+ * ```
666
+ */
667
+ clone(): HttpRequest<M, T>;
668
+ /**
669
+ * Sends the request (with retries, if configured) and resolves with the {@link ResponseWrapper} of a
670
+ * successful (2xx or opaque) response. Any failure — non-2xx status, network error, timeout, abort,
671
+ * interceptor error — rejects with a {@link RequestError}; use `getResult()` for a non-throwing variant.
672
+ */
673
+ getResponse(): Promise<ResponseWrapper<T>>;
674
+ private _retriable;
675
+ private _attempt;
676
+ /** Attaches the CSRF token (see {@link withCsrf}) to `config.headers` when the final URL qualifies. */
677
+ private _csrf;
678
+ /**
679
+ * Sends the request and parses the body as JSON — see {@link ResponseWrapper.getJson} for the empty-body
680
+ * rule and schema validation.
681
+ *
682
+ * @example
683
+ * ```typescript
684
+ * const users = await api.get("/users").getJson<User[]>();
685
+ * const user = await api.get("/me").getJson(UserSchema); // validated + typed by the schema
686
+ * ```
687
+ */
688
+ getJson<U = T>(): Promise<U>;
689
+ getJson<S extends StandardSchemaV1>(schema: S): Promise<StandardSchemaV1.InferOutput<S>>;
690
+ /** Sends the request and returns the body as text. */
691
+ getText(): Promise<string>;
692
+ /** Sends the request and returns the body as a `Blob` (downloads, images). */
693
+ getBlob(): Promise<Blob>;
694
+ /** Sends the request and returns the body as an `ArrayBuffer`. */
695
+ getArrayBuffer(): Promise<ArrayBuffer>;
696
+ /** Sends the request and parses the body as `FormData`. */
697
+ getFormData(): Promise<FormData>;
698
+ /** Sends the request and returns the raw body stream — see {@link ResponseWrapper.getBody}. */
699
+ getBody(): Promise<ReadableStream<Uint8Array> | null>;
700
+ /**
701
+ * Sends the request, parses JSON and applies a selector — see {@link ResponseWrapper.getData}.
702
+ *
703
+ * @example
704
+ * ```typescript
705
+ * const names = await api.get<Page<User>>("/users").getData(page => page.items.map(u => u.name));
706
+ * ```
707
+ */
708
+ getData<U = T>(): Promise<U>;
709
+ getData<S extends StandardSchemaV1>(schema: S): Promise<StandardSchemaV1.InferOutput<S>>;
710
+ getData<S extends StandardSchemaV1, R>(schema: S, selector: (data: StandardSchemaV1.InferOutput<S>) => R): Promise<R>;
711
+ getData<R>(selector: (data: T) => R): Promise<R>;
712
+ getData<U, R>(selector: (data: U) => R): Promise<R>;
713
+ /**
714
+ * Sends the request and resolves with `{ data, error }` instead of throwing — for code that prefers
715
+ * errors as values. `error` is the same {@link RequestError} `getJson()` would have thrown.
716
+ *
717
+ * @example
718
+ * ```typescript
719
+ * const { data, error } = await api.get("/me").getResult<User>();
720
+ * if (error) return showError(error.message);
721
+ * render(data);
722
+ * ```
723
+ */
724
+ getResult<U = T>(): Promise<RequestResult<U>>;
725
+ getResult<S extends StandardSchemaV1>(schema: S): Promise<RequestResult<StandardSchemaV1.InferOutput<S>>>;
726
+ }
727
+ //#endregion
728
+ //#region src/api.d.ts
729
+ /** Chainable `HttpRequest` methods that make sense as api-wide defaults (everything but body, signal and clone). */
730
+ type ApiKeys = Exclude<{ [K in keyof HttpRequest]: K extends `with${string}` | "onRetry" ? K : never; }[keyof HttpRequest], "withBody" | "withGraphQL" | "withSignal" | "withAbortController">;
731
+ type ApiChainables = { [K in keyof HttpRequest as K extends ApiKeys ? K : never]: HttpRequest[K] extends ((...args: infer A) => unknown) ? (...args: A) => ApiBuilder : never; };
732
+ /**
733
+ * A set of defaults for the requests it creates: base URL, headers, auth, timeout, retries, interceptors —
734
+ * every `with*` method of {@link HttpRequest} except the body and signal ones, with the same signatures.
735
+ *
736
+ * Api instances are **immutable**: each `with*` call returns a new instance and leaves the original
737
+ * untouched, so an api can be specialised safely (`const admin = api.withHeader("X-Role", "admin")`).
738
+ * Requests created from it can still override any default. Arguments are applied to every request the
739
+ * api creates (an object passed to `withHeaders` is read each time, so treat it as frozen).
740
+ *
741
+ * @example
742
+ * ```typescript
743
+ * export const api = createApi()
744
+ * .withBaseURL("https://api.example.com")
745
+ * .withBearerToken(token)
746
+ * .withTimeout(5000)
747
+ * .withRetries(2);
748
+ *
749
+ * const users = await api.get("/users").getJson<User[]>();
750
+ * const user = await api.post("/users").withBody({ name: "Ada" }).getJson<User>();
751
+ * ```
752
+ */
753
+ interface ApiBuilder extends ApiChainables {
754
+ withHeaders<H extends { [K in keyof H]: string | number | null | undefined; }>(headers: H): ApiBuilder;
755
+ withCookies<C extends { [K in keyof C]: string; }>(cookies: C): ApiBuilder;
756
+ withQueryParams<P extends { [K in keyof P]: QueryValue; }>(params: P | URLSearchParams): ApiBuilder;
757
+ /**
758
+ * Prefixes relative paths with `baseURL`. Paths are *joined*, not resolved: `/v1` + `/users` → `/v1/users`,
759
+ * `./users` and `users` work the same, and absolute URLs (`https://…`, `//…`) are used as-is.
760
+ */
761
+ withBaseURL(baseURL: string): ApiBuilder;
762
+ /** Creates a GET request. `T` declares the JSON type the response is expected to have. */
763
+ get<T = unknown>(path?: string): HttpRequest<"GET", T>;
764
+ /** Creates a HEAD request. */
765
+ head<T = unknown>(path?: string): HttpRequest<"HEAD", T>;
766
+ /** Creates an OPTIONS request. */
767
+ options<T = unknown>(path?: string): HttpRequest<"OPTIONS", T>;
768
+ /** Creates a POST request. */
769
+ post<T = unknown>(path?: string): HttpRequest<"POST", T>;
770
+ /** Creates a PUT request. */
771
+ put<T = unknown>(path?: string): HttpRequest<"PUT", T>;
772
+ /** Creates a PATCH request. */
773
+ patch<T = unknown>(path?: string): HttpRequest<"PATCH", T>;
774
+ /** Creates a DELETE request (`del` is an alias). */
775
+ delete<T = unknown>(path?: string): HttpRequest<"DELETE", T>;
776
+ /** Alias of `delete`. */
777
+ del<T = unknown>(path?: string): HttpRequest<"DELETE", T>;
778
+ }
779
+ /**
780
+ * Creates an empty {@link ApiBuilder}. Configure it once, export it, and create every request through it.
781
+ * (`create.api()` is the same function.)
782
+ */
783
+ export declare function createApi(): ApiBuilder;
784
+ //#endregion
785
+ //#region src/index.d.ts
786
+ /** A GET request. */
787
+ export type GetRequest<T = unknown> = HttpRequest<"GET", T>;
788
+ /** A HEAD request. */
789
+ export type HeadRequest<T = unknown> = HttpRequest<"HEAD", T>;
790
+ /** An OPTIONS request. */
791
+ export type OptionsRequest<T = unknown> = HttpRequest<"OPTIONS", T>;
792
+ /** A POST request. */
793
+ export type PostRequest<T = unknown> = HttpRequest<"POST", T>;
794
+ /** A PUT request. */
795
+ export type PutRequest<T = unknown> = HttpRequest<"PUT", T>;
796
+ /** A PATCH request. */
797
+ export type PatchRequest<T = unknown> = HttpRequest<"PATCH", T>;
798
+ /** A DELETE request. */
799
+ export type DeleteRequest<T = unknown> = HttpRequest<"DELETE", T>;
800
+ /** Any request — the v1 name for {@link HttpRequest}. */
801
+ export type BaseRequest<T = unknown> = HttpRequest<Method, T>;
802
+ /** Any request that may carry a body — the v1 name for `HttpRequest<"POST" | "PUT" | "PATCH" | "DELETE">`. */
803
+ export type BodyRequest<T = unknown> = HttpRequest<BodyMethod, T>;
804
+ /** Creates a GET request. `T` declares the JSON type the response is expected to have. */
805
+ export declare const createGet: <T = unknown>(url: string) => GetRequest<T>;
806
+ /** Creates a HEAD request. */
807
+ export declare const createHead: <T = unknown>(url: string) => HeadRequest<T>;
808
+ /** Creates an OPTIONS request. */
809
+ export declare const createOptions: <T = unknown>(url: string) => OptionsRequest<T>;
810
+ /** Creates a POST request. */
811
+ export declare const createPost: <T = unknown>(url: string) => PostRequest<T>;
812
+ /** Creates a PUT request. */
813
+ export declare const createPut: <T = unknown>(url: string) => PutRequest<T>;
814
+ /** Creates a PATCH request. */
815
+ export declare const createPatch: <T = unknown>(url: string) => PatchRequest<T>;
816
+ /** Creates a DELETE request. */
817
+ export declare const createDelete: <T = unknown>(url: string) => DeleteRequest<T>;
818
+ /**
819
+ * The entry point: one factory per HTTP method plus `api()` for configured instances.
820
+ *
821
+ * @example
822
+ * ```typescript
823
+ * import create from "create-request";
824
+ *
825
+ * const users = await create.get("https://api.example.com/users").getJson<User[]>();
826
+ * const api = create.api().withBaseURL("https://api.example.com").withBearerToken(token);
827
+ * const me = await api.get("/me").getJson<User>();
828
+ * ```
829
+ */
830
+ declare const create: {
831
+ /** Creates a GET request. `T` declares the JSON type the response is expected to have. */
832
+ readonly get: <T = unknown>(url: string) => GetRequest<T>;
833
+ /** Creates a HEAD request. */
834
+ readonly head: <T = unknown>(url: string) => HeadRequest<T>;
835
+ /** Creates an OPTIONS request. */
836
+ readonly options: <T = unknown>(url: string) => OptionsRequest<T>;
837
+ /** Creates a POST request. */
838
+ readonly post: <T = unknown>(url: string) => PostRequest<T>;
839
+ /** Creates a PUT request. */
840
+ readonly put: <T = unknown>(url: string) => PutRequest<T>;
841
+ /** Creates a PATCH request. */
842
+ readonly patch: <T = unknown>(url: string) => PatchRequest<T>;
843
+ /** Creates a DELETE request. */
844
+ readonly delete: <T = unknown>(url: string) => DeleteRequest<T>;
845
+ /** Alias of `delete`. */
846
+ readonly del: <T = unknown>(url: string) => DeleteRequest<T>;
847
+ /** Creates an api instance — see {@link createApi}. */
848
+ readonly api: typeof createApi;
849
+ };
850
+ //#endregion
851
+ export { type ApiBuilder, type Body, type BodyMethod, type CookiesRecord, type CsrfOptions, type ErrorInterceptor, type FetchFunction, type GraphQLOptions, type HeadersRecord, type Method, type QueryParams, type QueryValue, type RequestConfig, type RequestErrorCode, type RequestErrorOptions, type RequestInterceptor, type RequestResult, type ResponseInterceptor, type RetryConfig, type RetryContext, type StandardSchemaV1, create as default };