@gcmdev/faxios 0.2.1 → 0.2.2
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/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/adapters/fetch-helpers.d.ts +1 -1
- package/dist/lib/adapters/fetch-helpers.d.ts.map +1 -1
- package/dist/lib/adapters/fetch.d.ts.map +1 -1
- package/dist/lib/adapters/fetch.js +86 -26
- package/dist/lib/adapters/fetch.js.map +1 -1
- package/dist/lib/cancel/CanceledError.d.ts +1 -1
- package/dist/lib/cancel/CanceledError.d.ts.map +1 -1
- package/dist/lib/core/Faxios.d.ts +3 -3
- package/dist/lib/core/Faxios.d.ts.map +1 -1
- package/dist/lib/core/Faxios.js +8 -4
- package/dist/lib/core/Faxios.js.map +1 -1
- package/dist/lib/core/FaxiosError.d.ts +2 -2
- package/dist/lib/core/FaxiosError.d.ts.map +1 -1
- package/dist/lib/core/FaxiosHeaders.d.ts +8 -8
- package/dist/lib/core/FaxiosHeaders.d.ts.map +1 -1
- package/dist/lib/core/define.d.ts +3 -3
- package/dist/lib/core/define.d.ts.map +1 -1
- package/dist/lib/core/dispatchRequest.d.ts +1 -1
- package/dist/lib/core/dispatchRequest.d.ts.map +1 -1
- package/dist/lib/core/mergeConfig.d.ts +2 -2
- package/dist/lib/core/mergeConfig.d.ts.map +1 -1
- package/dist/lib/core/mergeConfig.js +40 -2
- package/dist/lib/core/mergeConfig.js.map +1 -1
- package/dist/lib/core/route.d.ts +1 -1
- package/dist/lib/core/route.d.ts.map +1 -1
- package/dist/lib/core/route.js +2 -5
- package/dist/lib/core/route.js.map +1 -1
- package/dist/lib/core/transformData.d.ts +2 -2
- package/dist/lib/core/transformData.d.ts.map +1 -1
- package/dist/lib/core/validateSchema.d.ts +2 -2
- package/dist/lib/core/validateSchema.d.ts.map +1 -1
- package/dist/lib/env/data.d.ts +1 -1
- package/dist/lib/env/data.js +1 -1
- package/dist/lib/faxios.d.ts +50 -50
- package/dist/lib/faxios.d.ts.map +1 -1
- package/dist/lib/helpers/cloneConfig.d.ts +0 -1
- package/dist/lib/helpers/cloneConfig.d.ts.map +1 -1
- package/dist/lib/helpers/cloneConfig.js +2 -2
- package/dist/lib/helpers/cloneConfig.js.map +1 -1
- package/dist/lib/helpers/cookies.d.ts +1 -1
- package/dist/lib/helpers/cookies.d.ts.map +1 -1
- package/dist/lib/helpers/formDataToJSON.d.ts +1 -1
- package/dist/lib/helpers/formDataToJSON.d.ts.map +1 -1
- package/dist/lib/helpers/formDataToJSON.js +1 -1
- package/dist/lib/helpers/formDataToJSON.js.map +1 -1
- package/dist/lib/helpers/parseHeaders.d.ts +1 -1
- package/dist/lib/helpers/parseHeaders.d.ts.map +1 -1
- package/dist/lib/helpers/substitutePathParams.d.ts.map +1 -1
- package/dist/lib/helpers/substitutePathParams.js +11 -3
- package/dist/lib/helpers/substitutePathParams.js.map +1 -1
- package/dist/lib/helpers/toFormData.d.ts.map +1 -1
- package/dist/lib/helpers/toFormData.js +4 -2
- package/dist/lib/helpers/toFormData.js.map +1 -1
- package/dist/lib/helpers/validator.d.ts +1 -1
- package/dist/lib/helpers/validator.d.ts.map +1 -1
- package/dist/lib/platform.d.ts.map +1 -1
- package/dist/lib/plugins/authBearer.d.ts +9 -5
- package/dist/lib/plugins/authBearer.d.ts.map +1 -1
- package/dist/lib/plugins/authBearer.js +6 -0
- package/dist/lib/plugins/authBearer.js.map +1 -1
- package/dist/lib/plugins/definePlugin.d.ts +3 -3
- package/dist/lib/plugins/definePlugin.d.ts.map +1 -1
- package/dist/lib/plugins/retry.d.ts +23 -23
- package/dist/lib/plugins/retry.d.ts.map +1 -1
- package/dist/lib/plugins/timing.d.ts +8 -8
- package/dist/lib/plugins/timing.d.ts.map +1 -1
- package/dist/lib/types/standard-schema.d.ts +6 -6
- package/dist/lib/types/standard-schema.d.ts.map +1 -1
- package/dist/lib/types.d.ts +92 -92
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/utils.d.ts +4 -3
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +9 -2
- package/dist/lib/utils.js.map +1 -1
- package/package.json +7 -4
- package/skills/configure-requests/SKILL.md +8 -5
- package/skills/configure-requests/references/plugins.md +37 -6
- package/src/index.ts +15 -15
- package/src/lib/adapters/fetch-helpers.ts +3 -3
- package/src/lib/adapters/fetch.ts +125 -64
- package/src/lib/cancel/CanceledError.ts +1 -1
- package/src/lib/core/Faxios.ts +17 -13
- package/src/lib/core/FaxiosError.ts +3 -3
- package/src/lib/core/FaxiosHeaders.ts +16 -16
- package/src/lib/core/define.ts +7 -7
- package/src/lib/core/dispatchRequest.ts +2 -2
- package/src/lib/core/mergeConfig.ts +42 -4
- package/src/lib/core/prepareRequest.ts +1 -1
- package/src/lib/core/route.ts +3 -6
- package/src/lib/core/transformData.ts +2 -2
- package/src/lib/core/validateSchema.ts +4 -4
- package/src/lib/defaults/index.ts +1 -1
- package/src/lib/env/data.ts +1 -1
- package/src/lib/faxios.ts +55 -55
- package/src/lib/helpers/HttpStatusCode.ts +1 -1
- package/src/lib/helpers/cloneConfig.ts +2 -3
- package/src/lib/helpers/composeSignals.ts +4 -4
- package/src/lib/helpers/cookies.ts +3 -3
- package/src/lib/helpers/formDataToJSON.ts +4 -4
- package/src/lib/helpers/parseHeaders.ts +2 -2
- package/src/lib/helpers/progressEventReducer.ts +1 -1
- package/src/lib/helpers/sanitizeHeaderValue.ts +1 -1
- package/src/lib/helpers/substitutePathParams.ts +13 -3
- package/src/lib/helpers/throttle.ts +1 -1
- package/src/lib/helpers/toFormData.ts +11 -9
- package/src/lib/helpers/trackStream.ts +3 -3
- package/src/lib/helpers/validator.ts +3 -3
- package/src/lib/platform.ts +4 -4
- package/src/lib/plugins/authBearer.ts +14 -9
- package/src/lib/plugins/definePlugin.ts +3 -3
- package/src/lib/plugins/retry.ts +27 -27
- package/src/lib/plugins/timing.ts +5 -5
- package/src/lib/types/standard-schema.ts +6 -6
- package/src/lib/types.ts +125 -125
- package/src/lib/utils.ts +21 -12
|
@@ -86,8 +86,8 @@ const res = await faxios.get("https://api.example.com/users/{id}/posts", {
|
|
|
86
86
|
// GET https://api.example.com/users/42/posts?page=2&tag=news
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
-
`pathParams` values are `encodeURIComponent`-encoded. A missing or `null` value
|
|
90
|
-
`ERR_BAD_OPTION_VALUE`.
|
|
89
|
+
`pathParams` values are `encodeURIComponent`-encoded. A missing or `null` value, or `.`/`..`
|
|
90
|
+
(also percent-encoded, such as `%2e%2e`), rejects with `ERR_BAD_OPTION_VALUE`.
|
|
91
91
|
|
|
92
92
|
Binary downloads need `responseType`, and a type argument because `data` is `unknown`:
|
|
93
93
|
|
|
@@ -273,6 +273,7 @@ plugin's request options (such as `retry: false`). Before writing plugin code, r
|
|
|
273
273
|
`baseURL` origin when `baseURL` is absolute; otherwise only to relative request URLs. Use
|
|
274
274
|
`origins` to allow more. It leaves an explicit `Authorization` header alone unless
|
|
275
275
|
`overwrite: true`. Install `retry` before it if each attempt should call `getToken` again.
|
|
276
|
+
A `getToken` result that isn't a non-empty string sends the request without the header.
|
|
276
277
|
- `retry` defaults: 3 attempts, statuses 408/429/500/502/503/504 plus `ERR_NETWORK` and
|
|
277
278
|
`ETIMEDOUT`, exponential backoff from 100 ms with full jitter. A 429/503 `Retry-After` sets the
|
|
278
279
|
wait unless `respectRetryAfter: false`. Turn that off when something else, such as a queue
|
|
@@ -304,9 +305,11 @@ const { data } = await testApi.get("https://api.example.com/feed"); // { ok: tru
|
|
|
304
305
|
await faxios.get("https://api.example.com/feed", { fetchOptions: { cache: "no-store" } });
|
|
305
306
|
```
|
|
306
307
|
|
|
307
|
-
- `fetchOptions` is
|
|
308
|
-
`
|
|
309
|
-
`
|
|
308
|
+
- Every own enumerable key of `fetchOptions` is copied into a null-prototype init passed to
|
|
309
|
+
`fetch`, including runtime extensions (Next.js `next`, Workers `cf`, undici `dispatcher`).
|
|
310
|
+
Inherited keys are ignored. faxios sets `signal`, `method`, `headers`, `body`, `duplex` and
|
|
311
|
+
`credentials` itself and ignores those keys in `fetchOptions`.
|
|
312
|
+
- `env.fetch`, `env.Request` and `env.Response` count only as own properties of `env`.
|
|
310
313
|
- Plain-object values inside `fetchOptions` are copied when configs merge; class instances are
|
|
311
314
|
passed by reference.
|
|
312
315
|
- Proxies are runtime configuration, not a faxios option. In Node, the project docs route a
|
|
@@ -85,21 +85,49 @@ Check: `faxios.get("/orders", { trace: "x" })` on an instance without the plugin
|
|
|
85
85
|
installing it before `authBearer`:
|
|
86
86
|
|
|
87
87
|
```ts
|
|
88
|
-
import faxios, {
|
|
88
|
+
import faxios, { type FaxiosContext } from "@gcmdev/faxios";
|
|
89
89
|
import { definePlugin } from "@gcmdev/faxios/plugins";
|
|
90
90
|
import { authBearer, type AuthBearerCapability } from "@gcmdev/faxios/plugins/auth-bearer";
|
|
91
91
|
|
|
92
|
+
function requireSignIn() {
|
|
93
|
+
return definePlugin({
|
|
94
|
+
name: "requireSignIn",
|
|
95
|
+
middleware: async (ctx: FaxiosContext<unknown, AuthBearerCapability>, next) => {
|
|
96
|
+
if (!(await ctx.capabilities.auth.getToken())) throw new Error("Sign in first");
|
|
97
|
+
return next(ctx);
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const api = faxios
|
|
103
|
+
.create({ baseURL: "https://api.example.com" })
|
|
104
|
+
.use(authBearer(() => "token"))
|
|
105
|
+
.use(requireSignIn());
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Refresh a token after a 401
|
|
109
|
+
|
|
110
|
+
Install the refresh plugin before `authBearer` and never set `Authorization` in it. The retry
|
|
111
|
+
runs `authBearer` again, which calls `getToken` and applies its origin rule, so a third-party 401
|
|
112
|
+
is retried without the token. Share one in-flight refresh so rotating refresh tokens aren't spent twice:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import faxios, { FaxiosError } from "@gcmdev/faxios";
|
|
116
|
+
import { definePlugin } from "@gcmdev/faxios/plugins";
|
|
117
|
+
import { authBearer } from "@gcmdev/faxios/plugins/auth-bearer";
|
|
118
|
+
|
|
92
119
|
function refreshOn401(refresh: () => Promise<void>) {
|
|
120
|
+
let refreshing: Promise<void> | undefined;
|
|
93
121
|
return definePlugin({
|
|
94
122
|
name: "refreshOn401",
|
|
95
|
-
middleware: async (ctx
|
|
123
|
+
middleware: async (ctx, next) => {
|
|
96
124
|
try {
|
|
97
125
|
return await next(ctx);
|
|
98
126
|
}
|
|
99
127
|
catch (error) {
|
|
100
128
|
if (!(error instanceof FaxiosError) || error.response?.status !== 401) throw error;
|
|
101
|
-
|
|
102
|
-
|
|
129
|
+
refreshing ??= refresh().finally(() => { refreshing = undefined; });
|
|
130
|
+
await refreshing;
|
|
103
131
|
return next(ctx);
|
|
104
132
|
}
|
|
105
133
|
},
|
|
@@ -109,10 +137,13 @@ function refreshOn401(refresh: () => Promise<void>) {
|
|
|
109
137
|
let token = "old";
|
|
110
138
|
const api = faxios
|
|
111
139
|
.create({ baseURL: "https://api.example.com" })
|
|
112
|
-
.use(
|
|
113
|
-
.use(
|
|
140
|
+
.use(refreshOn401(async () => { token = "new"; }))
|
|
141
|
+
.use(authBearer(() => token));
|
|
114
142
|
```
|
|
115
143
|
|
|
144
|
+
Calling `next` again re-sends the body. A stream body is used up by the first try; sending it
|
|
145
|
+
again rejects with `ERR_BAD_REQUEST`.
|
|
146
|
+
|
|
116
147
|
## Rules `.use()` enforces at runtime
|
|
117
148
|
|
|
118
149
|
- `name` must be a non-empty own string, otherwise `ERR_BAD_OPTION_VALUE`.
|
package/src/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type { CreateFaxiosDefaults } from "./lib/types.ts";
|
|
|
11
11
|
export { default as Faxios } from "./lib/core/Faxios.ts";
|
|
12
12
|
export { default as FaxiosError, isSchemaValidationError } from "./lib/core/FaxiosError.ts";
|
|
13
13
|
export type { SchemaValidationError } from "./lib/core/FaxiosError.ts";
|
|
14
|
-
export { default as
|
|
14
|
+
export { default as Cancel, default as CanceledError } from "./lib/cancel/CanceledError.ts";
|
|
15
15
|
export { default as isCancel } from "./lib/cancel/isCancel.ts";
|
|
16
16
|
export { default as isFaxiosError } from "./lib/helpers/isFaxiosError.ts";
|
|
17
17
|
export { default as toFormData } from "./lib/helpers/toFormData.ts";
|
|
@@ -23,28 +23,28 @@ export { VERSION } from "./lib/env/data.ts";
|
|
|
23
23
|
export type { FaxiosInstance, FaxiosStatic } from "./lib/faxios.ts";
|
|
24
24
|
export type { StandardSchemaV1 } from "./lib/types/standard-schema.ts";
|
|
25
25
|
export type { BasePerCallConfig, DefineConfig, DefinedEndpoint, FaxiosLike, PerCallConfig } from "./lib/core/define.ts";
|
|
26
|
-
export type { RouteConfig, RouteMethodConfig
|
|
26
|
+
export type { RouteBuilder, RouteConfig, RouteMethodConfig } from "./lib/core/route.ts";
|
|
27
27
|
|
|
28
28
|
export type {
|
|
29
|
+
CreateFaxiosDefaults,
|
|
30
|
+
FaxiosConfigHeaders,
|
|
31
|
+
FaxiosContext,
|
|
32
|
+
FaxiosHeaderValue,
|
|
33
|
+
FaxiosMiddleware,
|
|
34
|
+
FaxiosNext,
|
|
35
|
+
FaxiosPlugin,
|
|
36
|
+
FaxiosPromise,
|
|
29
37
|
FaxiosRequestConfig,
|
|
30
|
-
InternalFaxiosRequestConfig,
|
|
31
|
-
FaxiosResponse,
|
|
32
38
|
FaxiosRequestHeaders,
|
|
39
|
+
FaxiosResponse,
|
|
33
40
|
FaxiosResponseHeadersLike,
|
|
34
|
-
FaxiosHeaderValue,
|
|
35
|
-
Method,
|
|
36
|
-
ResponseType,
|
|
37
41
|
GenericHTMLFormElement,
|
|
42
|
+
HeadersDefaults,
|
|
43
|
+
InternalFaxiosRequestConfig,
|
|
44
|
+
Method,
|
|
38
45
|
RawFaxiosRequestConfig,
|
|
39
|
-
FaxiosPromise,
|
|
40
|
-
CreateFaxiosDefaults,
|
|
41
|
-
FaxiosConfigHeaders,
|
|
42
46
|
RawFaxiosRequestHeaders,
|
|
43
|
-
|
|
44
|
-
FaxiosContext,
|
|
45
|
-
FaxiosNext,
|
|
46
|
-
FaxiosMiddleware,
|
|
47
|
-
FaxiosPlugin
|
|
47
|
+
ResponseType
|
|
48
48
|
} from "./lib/types.ts";
|
|
49
49
|
|
|
50
50
|
// Instance-synthesized members (no 1:1 source module). Types are self-contained
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
import FaxiosError from "../core/FaxiosError.js";
|
|
4
4
|
import type FaxiosHeaders from "../core/FaxiosHeaders.js";
|
|
5
5
|
import type {
|
|
6
|
-
InternalFaxiosRequestConfig,
|
|
7
6
|
FaxiosRequestHeaders,
|
|
8
|
-
FaxiosResponse
|
|
7
|
+
FaxiosResponse,
|
|
8
|
+
InternalFaxiosRequestConfig
|
|
9
9
|
} from "../types.js";
|
|
10
10
|
import utils from "../utils.js";
|
|
11
11
|
|
|
@@ -144,7 +144,7 @@ export const cleanFormDataContentType = (
|
|
|
144
144
|
headers: FaxiosRequestHeaders
|
|
145
145
|
): void => {
|
|
146
146
|
if (utils.isFormData(data)) {
|
|
147
|
-
const contentType = headers.getContentType() as
|
|
147
|
+
const contentType = headers.getContentType() as null | string | undefined;
|
|
148
148
|
if (
|
|
149
149
|
contentType &&
|
|
150
150
|
/^multipart\/form-data/i.test(contentType) &&
|
|
@@ -6,19 +6,19 @@ import { VERSION } from "../env/data.js";
|
|
|
6
6
|
import composeSignals from "../helpers/composeSignals.js";
|
|
7
7
|
import estimateDataURLDecodedBytes from "../helpers/estimateDataURLDecodedBytes.js";
|
|
8
8
|
import {
|
|
9
|
-
|
|
9
|
+
asyncDecorator,
|
|
10
10
|
progressEventDecorator,
|
|
11
|
-
|
|
11
|
+
progressEventReducer
|
|
12
12
|
} from "../helpers/progressEventReducer.js";
|
|
13
13
|
import { toByteStringHeaderObject } from "../helpers/sanitizeHeaderValue.js";
|
|
14
14
|
import { trackStream } from "../helpers/trackStream.js";
|
|
15
15
|
import platform from "../platform.js";
|
|
16
16
|
import type {
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
FaxiosRequestHeaders,
|
|
18
|
+
InternalFaxiosRequestConfig
|
|
19
19
|
} from "../types.js";
|
|
20
20
|
import utils from "../utils.js";
|
|
21
|
-
import { checkDeclaredContentLength, checkMaterializedSize,
|
|
21
|
+
import { checkDeclaredContentLength, checkMaterializedSize, cleanFormDataContentType, encodeBodyIfNeeded, handleFetchCaughtError } from "./fetch-helpers.js";
|
|
22
22
|
import type { ComposedSignal, PendingBodyErrorRef } from "./fetch-helpers.js";
|
|
23
23
|
|
|
24
24
|
// btoa is a global in Node 16+ and browsers; accessed via globalThis for no-DOM lib compat
|
|
@@ -28,29 +28,29 @@ const _btoa: (data: string) => string = (
|
|
|
28
28
|
type AnyConstructor = new (...args: Array<unknown>) => unknown;
|
|
29
29
|
type FetchFn = (input: unknown, init?: unknown) => Promise<AnyResponse>;
|
|
30
30
|
type AnyResponse = {
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
arrayBuffer: () => Promise<ArrayBuffer>;
|
|
31
33
|
body: unknown;
|
|
32
34
|
headers: {
|
|
33
|
-
has: (name: string) => boolean;
|
|
34
|
-
get: (name: string) => string | null;
|
|
35
35
|
[key: string]: unknown;
|
|
36
|
+
get: (name: string) => null | string;
|
|
37
|
+
has: (name: string) => boolean;
|
|
36
38
|
};
|
|
37
39
|
status: number;
|
|
38
40
|
statusText: string;
|
|
39
|
-
arrayBuffer: () => Promise<ArrayBuffer>;
|
|
40
|
-
[key: string]: unknown;
|
|
41
41
|
};
|
|
42
42
|
type AnyRequest = {
|
|
43
|
+
[key: string]: unknown;
|
|
44
|
+
arrayBuffer: () => Promise<ArrayBuffer>;
|
|
45
|
+
body: null | { cancel: () => Promise<void>; };
|
|
43
46
|
headers: {
|
|
47
|
+
get: (name: string) => null | string;
|
|
44
48
|
has: (name: string) => boolean;
|
|
45
|
-
get: (name: string) => string | null;
|
|
46
49
|
};
|
|
47
|
-
body: { cancel: () => Promise<void>; } | null;
|
|
48
|
-
arrayBuffer: () => Promise<ArrayBuffer>;
|
|
49
|
-
[key: string]: unknown;
|
|
50
50
|
};
|
|
51
51
|
type AnyReadableStream = {
|
|
52
|
-
cancel: () => Promise<void>;
|
|
53
52
|
[key: string]: unknown;
|
|
53
|
+
cancel: () => Promise<void>;
|
|
54
54
|
};
|
|
55
55
|
type AnyTextEncoder = { encode: (str: string) => Uint8Array; };
|
|
56
56
|
|
|
@@ -58,6 +58,29 @@ const DEFAULT_CHUNK_SIZE = 64 * 1024;
|
|
|
58
58
|
|
|
59
59
|
const { isFunction } = utils;
|
|
60
60
|
|
|
61
|
+
// Web stream bodies this adapter has sent. fetch can read a stream without leaving it locked,
|
|
62
|
+
// and there's no portable "disturbed" check, so a second send is caught here.
|
|
63
|
+
const sentStreamBodies = new WeakSet<object>();
|
|
64
|
+
|
|
65
|
+
// A stream body can be read once. Middleware that calls next() again after a try read it
|
|
66
|
+
// would send an empty body (Node stream) or get an uncoded TypeError from fetch (web stream).
|
|
67
|
+
const rejectUsedStreamBody = (data: unknown, config: InternalFaxiosRequestConfig): void => {
|
|
68
|
+
const body = data as { bodyUsed?: unknown; destroyed?: unknown; locked?: unknown; readableDidRead?: unknown; readableEnded?: unknown; };
|
|
69
|
+
const isWebStream = utils.isReadableStream!(data);
|
|
70
|
+
if (
|
|
71
|
+
(isWebStream && (body.locked === true || sentStreamBodies.has(body))) ||
|
|
72
|
+
((utils.isRequest!(data) || utils.isResponse!(data)) && body.bodyUsed === true) ||
|
|
73
|
+
(utils.isStream(data) && (body.readableDidRead === true || body.readableEnded === true || body.destroyed === true))
|
|
74
|
+
) {
|
|
75
|
+
throw new FaxiosError(
|
|
76
|
+
"Request body stream was already used and can't be sent again; pass a new body for each try",
|
|
77
|
+
FaxiosError.ERR_BAD_REQUEST,
|
|
78
|
+
config
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (isWebStream) sentStreamBodies.add(body);
|
|
82
|
+
};
|
|
83
|
+
|
|
61
84
|
// Server runtimes default User-Agent to their own (e.g. "node"), so name faxios there.
|
|
62
85
|
// Browsers send their own User-Agent; setting one makes every cross-origin request
|
|
63
86
|
// need a CORS preflight that most APIs don't allow.
|
|
@@ -116,8 +139,40 @@ const maybeWithAuthCredentials = (url: string): boolean => {
|
|
|
116
139
|
return urlToCheck.includes("@");
|
|
117
140
|
};
|
|
118
141
|
|
|
142
|
+
// RequestInit members faxios sets itself; the same keys in `fetchOptions` are ignored.
|
|
143
|
+
const FAXIOS_INIT_KEYS: Record<string, true> = {
|
|
144
|
+
body: true,
|
|
145
|
+
credentials: true,
|
|
146
|
+
duplex: true,
|
|
147
|
+
headers: true,
|
|
148
|
+
method: true,
|
|
149
|
+
signal: true,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Spec defaults written as own init members when the caller sets none. undici converts the init
|
|
153
|
+
// into a plain dictionary and reads absent members from it, so an absent member still picks up a
|
|
154
|
+
// polluted Object.prototype value; an own default leaves nothing to inherit.
|
|
155
|
+
const REQUEST_INIT_DEFAULTS: Record<string, unknown> = {
|
|
156
|
+
cache: "default",
|
|
157
|
+
integrity: "",
|
|
158
|
+
keepalive: false,
|
|
159
|
+
mode: "cors",
|
|
160
|
+
redirect: "follow",
|
|
161
|
+
referrer: "about:client",
|
|
162
|
+
referrerPolicy: "",
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Where undici (Node's fetch) keeps its global dispatcher.
|
|
166
|
+
const UNDICI_GLOBAL_DISPATCHER = Symbol.for("undici.globalDispatcher.1");
|
|
167
|
+
|
|
168
|
+
type FetchEnv = {
|
|
169
|
+
fetch?: unknown;
|
|
170
|
+
Request?: unknown;
|
|
171
|
+
Response?: unknown;
|
|
172
|
+
};
|
|
173
|
+
|
|
119
174
|
// eslint-disable-next-line sonarjs/function-return-type
|
|
120
|
-
const factory = (env:
|
|
175
|
+
const factory = (env: FetchEnv) => {
|
|
121
176
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
122
177
|
const globalObject: Record<string, unknown> = utils.global ?? globalThis;
|
|
123
178
|
const ReadableStream = globalObject["ReadableStream"] as
|
|
@@ -127,26 +182,10 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
127
182
|
| (new () => AnyTextEncoder)
|
|
128
183
|
| undefined;
|
|
129
184
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
{
|
|
135
|
-
Request: globalObject["Request"],
|
|
136
|
-
Response: globalObject["Response"],
|
|
137
|
-
},
|
|
138
|
-
env
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
const {
|
|
142
|
-
fetch: envFetch,
|
|
143
|
-
Request,
|
|
144
|
-
Response,
|
|
145
|
-
} = env as {
|
|
146
|
-
fetch?: FetchFn;
|
|
147
|
-
Request?: AnyConstructor;
|
|
148
|
-
Response?: AnyConstructor;
|
|
149
|
-
};
|
|
185
|
+
// `undefined` falls back to the global constructor; `null` disables it.
|
|
186
|
+
const envFetch = env.fetch as FetchFn | undefined;
|
|
187
|
+
const Request = (env.Request === undefined ? globalObject["Request"] : env.Request) as AnyConstructor | undefined;
|
|
188
|
+
const Response = (env.Response === undefined ? globalObject["Response"] : env.Response) as AnyConstructor | undefined;
|
|
150
189
|
const _globalFetch = (globalThis as unknown as Record<string, unknown>)[
|
|
151
190
|
"fetch"
|
|
152
191
|
];
|
|
@@ -160,6 +199,31 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
160
199
|
return false;
|
|
161
200
|
}
|
|
162
201
|
|
|
202
|
+
// Only members this runtime's Request exposes, so an engine that rejects members it doesn't
|
|
203
|
+
// implement (Cloudflare Workers) is never sent one.
|
|
204
|
+
const requestInitDefaults = Object.entries(REQUEST_INIT_DEFAULTS).filter(([ key ]) =>
|
|
205
|
+
isRequestSupported && utils.hasOwnInPrototypeChain((Request as AnyConstructor).prototype, key));
|
|
206
|
+
|
|
207
|
+
// Null prototype: WebIDL reads every RequestInit member with Get, which walks the prototype
|
|
208
|
+
// chain, so a plain object literal would let a polluted Object.prototype.redirect, .integrity,
|
|
209
|
+
// .mode or undici .dispatcher apply to every request. Every own enumerable key of fetchOptions
|
|
210
|
+
// is copied, so runtime extensions (Next.js `next`, Workers `cf`, Bun `tls`) still pass through;
|
|
211
|
+
// inherited keys never do. The caller sets signal, method, headers, body, duplex, credentials.
|
|
212
|
+
const initFromFetchOptions = (fetchOptions: unknown): Record<string, unknown> => {
|
|
213
|
+
const init: Record<string, unknown> = Object.create(null);
|
|
214
|
+
for (const [ key, value ] of requestInitDefaults) init[key] = value;
|
|
215
|
+
// Node's global fetch reads `dispatcher` the same way; pin it to undici's own global one.
|
|
216
|
+
if (!envFetch && utils.hasOwnProp(globalThis, UNDICI_GLOBAL_DISPATCHER)) {
|
|
217
|
+
init["dispatcher"] = (globalThis as Record<PropertyKey, unknown>)[UNDICI_GLOBAL_DISPATCHER];
|
|
218
|
+
}
|
|
219
|
+
if (utils.isObject(fetchOptions)) {
|
|
220
|
+
for (const key of Object.keys(fetchOptions)) {
|
|
221
|
+
if (!utils.hasOwnProp(FAXIOS_INIT_KEYS, key)) init[key] = (fetchOptions as Record<string, unknown>)[key];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return init;
|
|
225
|
+
};
|
|
226
|
+
|
|
163
227
|
const isReadableStreamSupported = isFunction(ReadableStream);
|
|
164
228
|
|
|
165
229
|
const encodeText: (str: string) => Promise<Uint8Array> | Uint8Array =
|
|
@@ -179,7 +243,7 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
179
243
|
// `fetch(request, init)`. undici (Node 26.11+) starts reading an async iterable in both,
|
|
180
244
|
// so the first chunk goes to the discarded copy. One web stream is read only once.
|
|
181
245
|
// Web stream bodies must yield bytes, so string chunks (e.g. `Readable.from(["a"])`) are encoded.
|
|
182
|
-
const from = (ReadableStream as { from?: (source: unknown) => unknown; }
|
|
246
|
+
const from = (ReadableStream as undefined | { from?: (source: unknown) => unknown; })?.from;
|
|
183
247
|
const streamFrom = typeof from === "function" ? from.bind(ReadableStream) : undefined;
|
|
184
248
|
async function* toByteChunks(source: AsyncIterable<unknown>) {
|
|
185
249
|
for await (const chunk of source) yield typeof chunk === "string" ? await encodeText(chunk) : chunk;
|
|
@@ -269,7 +333,7 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
269
333
|
}
|
|
270
334
|
|
|
271
335
|
if (utils.isArrayBufferView(body) || utils.isArrayBuffer(body)) {
|
|
272
|
-
return (body as
|
|
336
|
+
return (body as ArrayBuffer | ArrayBufferView).byteLength;
|
|
273
337
|
}
|
|
274
338
|
|
|
275
339
|
if (utils.isURLSearchParams(body)) {
|
|
@@ -291,7 +355,7 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
291
355
|
return length == null ? getBodyLength(body) : length;
|
|
292
356
|
};
|
|
293
357
|
|
|
294
|
-
const normalizeCredentials = (wc:
|
|
358
|
+
const normalizeCredentials = (wc: boolean | string): string => {
|
|
295
359
|
if (utils.isString(wc)) return wc as string;
|
|
296
360
|
return wc ? "include" : "omit";
|
|
297
361
|
};
|
|
@@ -303,7 +367,7 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
303
367
|
headers: FaxiosRequestHeaders,
|
|
304
368
|
own: (key: string) => unknown
|
|
305
369
|
): string => {
|
|
306
|
-
let auth: {
|
|
370
|
+
let auth: undefined | { password: unknown; username: unknown; };
|
|
307
371
|
const configAuth = own("auth");
|
|
308
372
|
if (configAuth) {
|
|
309
373
|
auth = {
|
|
@@ -398,7 +462,7 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
398
462
|
onUploadProgress: unknown,
|
|
399
463
|
_trackStream: (stream: unknown, onProgress?: (bytes: number) => void, flush?: () => void) => unknown
|
|
400
464
|
): unknown => {
|
|
401
|
-
let contentTypeHeader:
|
|
465
|
+
let contentTypeHeader: null | string;
|
|
402
466
|
if (
|
|
403
467
|
utils.isFormData(data) &&
|
|
404
468
|
(contentTypeHeader = _request.headers.get("content-type"))
|
|
@@ -587,7 +651,7 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
587
651
|
return async (config: InternalFaxiosRequestConfig) => {
|
|
588
652
|
const _resolved = prepareRequest(config) as InternalFaxiosRequestConfig & {
|
|
589
653
|
fetchOptions?: Record<string, unknown>;
|
|
590
|
-
withCredentials?:
|
|
654
|
+
withCredentials?: boolean | string;
|
|
591
655
|
};
|
|
592
656
|
let {
|
|
593
657
|
url,
|
|
@@ -604,7 +668,7 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
604
668
|
maxBodyLength,
|
|
605
669
|
} = _resolved;
|
|
606
670
|
let headers = _resolved.headers;
|
|
607
|
-
let withCredentials:
|
|
671
|
+
let withCredentials: boolean | string =
|
|
608
672
|
_resolved.withCredentials ?? "same-origin";
|
|
609
673
|
|
|
610
674
|
const hasMaxContentLength =
|
|
@@ -638,10 +702,12 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
638
702
|
|
|
639
703
|
const unsubscribe = composedSignal?.unsubscribe?.bind(composedSignal);
|
|
640
704
|
|
|
641
|
-
let request:
|
|
705
|
+
let request: null | Request = null;
|
|
642
706
|
const pendingBodyErrorRef: PendingBodyErrorRef = { value: null };
|
|
643
707
|
|
|
644
708
|
try {
|
|
709
|
+
rejectUsedStreamBody(data, config);
|
|
710
|
+
|
|
645
711
|
url = applyAuthToRequest(url!, headers, own);
|
|
646
712
|
|
|
647
713
|
const preflightContentLength = await checkPreflightLimits(
|
|
@@ -696,25 +762,20 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
696
762
|
// Case-insensitive check: toByteStringHeaderObject may normalize to lowercase.
|
|
697
763
|
data = toWebStream(await encodeBodyIfNeeded(data, serializedHeaders, encodeText));
|
|
698
764
|
|
|
699
|
-
const resolvedOptions =
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
credentials: isCredentialsSupported ? withCredentials : undefined,
|
|
707
|
-
};
|
|
765
|
+
const resolvedOptions = initFromFetchOptions(fetchOptions);
|
|
766
|
+
resolvedOptions["signal"] = composedSignal;
|
|
767
|
+
resolvedOptions["method"] = (method as string).toUpperCase();
|
|
768
|
+
resolvedOptions["headers"] = serializedHeaders;
|
|
769
|
+
resolvedOptions["body"] = data;
|
|
770
|
+
resolvedOptions["duplex"] = "half";
|
|
771
|
+
resolvedOptions["credentials"] = isCredentialsSupported ? withCredentials : undefined;
|
|
708
772
|
|
|
709
773
|
request = isRequestSupported
|
|
710
774
|
? new (Request as AnyConstructor)(url, resolvedOptions) as Request
|
|
711
775
|
: null;
|
|
712
776
|
|
|
713
|
-
// Pass resolvedOptions
|
|
714
|
-
//
|
|
715
|
-
// headers, body, credentials) off Object.prototype when init is absent,
|
|
716
|
-
// so a polluted Object.prototype.method would override the Request's verb.
|
|
717
|
-
// An explicit own-property init neutralizes every such gadget.
|
|
777
|
+
// Pass resolvedOptions as the fetch init even when a Request object is supplied, so the
|
|
778
|
+
// same null-prototype members apply to both calls.
|
|
718
779
|
let response = await (isRequestSupported
|
|
719
780
|
? _fetch(request, resolvedOptions)
|
|
720
781
|
: _fetch(url, resolvedOptions));
|
|
@@ -778,12 +839,12 @@ const factory = (env: Record<string, unknown>) => {
|
|
|
778
839
|
const seedCache = new Map<unknown, unknown>();
|
|
779
840
|
|
|
780
841
|
export const getFetch = (config?: { env?: Record<string, unknown>; }) => {
|
|
781
|
-
|
|
782
|
-
const
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
842
|
+
// Own reads only: a polluted Object.prototype.fetch must never become the transport.
|
|
843
|
+
const env = config && utils.hasOwnProp(config, "env") ? config.env : undefined;
|
|
844
|
+
const own = (key: keyof FetchEnv): unknown => (env && utils.hasOwnProp(env, key) ? env[key] : undefined);
|
|
845
|
+
const fetch = own("fetch");
|
|
846
|
+
const Request = own("Request");
|
|
847
|
+
const Response = own("Response");
|
|
787
848
|
const seeds: Array<unknown> = [ Request, Response, fetch ];
|
|
788
849
|
|
|
789
850
|
const len = seeds.length;
|
|
@@ -797,7 +858,7 @@ export const getFetch = (config?: { env?: Record<string, unknown>; }) => {
|
|
|
797
858
|
target = map.get(seed);
|
|
798
859
|
|
|
799
860
|
if (target === undefined) {
|
|
800
|
-
target = i ? new Map<unknown, unknown>() : factory(
|
|
861
|
+
target = i ? new Map<unknown, unknown>() : factory({ fetch, Request, Response });
|
|
801
862
|
map.set(seed, target);
|
|
802
863
|
}
|
|
803
864
|
|
|
@@ -13,7 +13,7 @@ class CanceledError extends FaxiosError {
|
|
|
13
13
|
*
|
|
14
14
|
* @returns {CanceledError} The created error.
|
|
15
15
|
*/
|
|
16
|
-
constructor(message?:
|
|
16
|
+
constructor(message?: null | string, config?: FaxiosRequestConfig, request?: unknown) {
|
|
17
17
|
super(message == null ? "canceled" : message, FaxiosError.ERR_CANCELED, config as InternalFaxiosRequestConfig, request);
|
|
18
18
|
this.name = "CanceledError";
|
|
19
19
|
(this as unknown as Record<string, unknown>)["__CANCEL__"] = true;
|
package/src/lib/core/Faxios.ts
CHANGED
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import CanceledError from "../cancel/CanceledError.js";
|
|
4
4
|
import buildURL from "../helpers/buildURL.js";
|
|
5
|
-
import cloneConfig
|
|
5
|
+
import cloneConfig from "../helpers/cloneConfig.js";
|
|
6
6
|
import validator from "../helpers/validator.js";
|
|
7
7
|
import type { ValidatorFn } from "../helpers/validator.js";
|
|
8
|
-
import type { StandardSchemaV1 } from "../types/standard-schema.js";
|
|
9
8
|
import type {
|
|
10
9
|
FaxiosContext,
|
|
11
10
|
FaxiosMiddleware,
|
|
@@ -21,6 +20,7 @@ import type {
|
|
|
21
20
|
SchemaConfig,
|
|
22
21
|
StringLiteralsOrString
|
|
23
22
|
} from "../types.js";
|
|
23
|
+
import type { StandardSchemaV1 } from "../types/standard-schema.js";
|
|
24
24
|
import utils from "../utils.js";
|
|
25
25
|
import buildFullPath from "./buildFullPath.js";
|
|
26
26
|
import { createDefinedEndpoint } from "./define.js";
|
|
@@ -29,21 +29,21 @@ import dispatchRequest from "./dispatchRequest.js";
|
|
|
29
29
|
import FaxiosError from "./FaxiosError.js";
|
|
30
30
|
import FaxiosHeaders from "./FaxiosHeaders.js";
|
|
31
31
|
import mergeConfig from "./mergeConfig.js";
|
|
32
|
-
import type {
|
|
32
|
+
import type { RouteBuilder, RouteConfig } from "./route.js";
|
|
33
33
|
import { createRouteBuilder } from "./route.js";
|
|
34
34
|
|
|
35
35
|
type TransitionalFn = (
|
|
36
|
-
validator:
|
|
36
|
+
validator: false | undefined | ValidatorFn,
|
|
37
37
|
version?: string,
|
|
38
38
|
message?: string
|
|
39
39
|
) => ValidatorFn;
|
|
40
40
|
type SpellingFn = (correctSpelling: string) => ValidatorFn;
|
|
41
41
|
const validators = validator.validators as Record<
|
|
42
42
|
string,
|
|
43
|
-
|
|
43
|
+
undefined | ValidatorFn
|
|
44
44
|
> & {
|
|
45
|
-
transitional?: TransitionalFn;
|
|
46
45
|
spelling?: SpellingFn;
|
|
46
|
+
transitional?: TransitionalFn;
|
|
47
47
|
};
|
|
48
48
|
|
|
49
49
|
// Every method header group declared on HeadersDefaults. `satisfies` keeps this
|
|
@@ -133,7 +133,10 @@ function resolveAllowAbsoluteUrls(
|
|
|
133
133
|
defaults: FaxiosRequestConfig
|
|
134
134
|
): void {
|
|
135
135
|
if (config.allowAbsoluteUrls === undefined) {
|
|
136
|
-
|
|
136
|
+
// Own reads only: instance defaults may be a plain object (the default instance's are).
|
|
137
|
+
const allowAbsoluteUrls = utils.hasOwnProp(defaults, "allowAbsoluteUrls") ? defaults.allowAbsoluteUrls : undefined;
|
|
138
|
+
const baseURL = utils.hasOwnProp(defaults, "baseURL") ? defaults.baseURL : undefined;
|
|
139
|
+
config.allowAbsoluteUrls = allowAbsoluteUrls ?? !baseURL;
|
|
137
140
|
}
|
|
138
141
|
}
|
|
139
142
|
|
|
@@ -143,10 +146,10 @@ type AnyContextMiddleware = FaxiosMiddleware<unknown, unknown>;
|
|
|
143
146
|
type AnyContextNext = FaxiosNext<unknown, unknown>;
|
|
144
147
|
|
|
145
148
|
type MiddlewareEntry = {
|
|
149
|
+
provided: Array<string>;
|
|
146
150
|
// The value passed to use(), kept only for eject()'s identity check.
|
|
147
151
|
ref: unknown;
|
|
148
152
|
run: AnyContextMiddleware;
|
|
149
|
-
provided: Array<string>;
|
|
150
153
|
};
|
|
151
154
|
|
|
152
155
|
function composeMiddleware(entries: ReadonlyArray<MiddlewareEntry>, dispatch: AnyContextNext): AnyContextNext {
|
|
@@ -272,7 +275,7 @@ class Faxios {
|
|
|
272
275
|
|
|
273
276
|
#addCapabilities(pluginName: string, provides: unknown): Array<string> {
|
|
274
277
|
const source = provides as Record<string, unknown>;
|
|
275
|
-
const keys = Object.keys(source).filter(key => !
|
|
278
|
+
const keys = Object.keys(source).filter(key => !utils.isDangerousKey(key));
|
|
276
279
|
for (const key of keys) {
|
|
277
280
|
if (utils.hasOwnProp(this.#capabilities, key)) {
|
|
278
281
|
throw new FaxiosError(`Plugin "${pluginName}" provides capability "${key}", which another plugin already provides`, FaxiosError.ERR_BAD_OPTION);
|
|
@@ -295,9 +298,9 @@ class Faxios {
|
|
|
295
298
|
*/
|
|
296
299
|
|
|
297
300
|
async request<O, D = unknown>(config: SchemaConfig<O, D>): Promise<FaxiosResponse<O, D>>;
|
|
298
|
-
async request<T = unknown, R = FaxiosResponse<T>, D = unknown>(configOrUrl:
|
|
301
|
+
async request<T = unknown, R = FaxiosResponse<T>, D = unknown>(configOrUrl: FaxiosRequestConfig<D> | string, config?: FaxiosRequestConfig<D>): Promise<R>;
|
|
299
302
|
async request(
|
|
300
|
-
configOrUrl:
|
|
303
|
+
configOrUrl: FaxiosRequestConfig | string,
|
|
301
304
|
config?: FaxiosRequestConfig
|
|
302
305
|
) {
|
|
303
306
|
try {
|
|
@@ -313,7 +316,7 @@ class Faxios {
|
|
|
313
316
|
}
|
|
314
317
|
|
|
315
318
|
async #request(
|
|
316
|
-
configOrUrl:
|
|
319
|
+
configOrUrl: FaxiosRequestConfig | string,
|
|
317
320
|
config?: FaxiosRequestConfig
|
|
318
321
|
): Promise<unknown> {
|
|
319
322
|
/*eslint no-param-reassign:0*/
|
|
@@ -361,8 +364,9 @@ class Faxios {
|
|
|
361
364
|
);
|
|
362
365
|
|
|
363
366
|
// Set config.method
|
|
367
|
+
const defaultMethod = utils.hasOwnProp(this.defaults, "method") ? this.defaults.method : undefined;
|
|
364
368
|
config.method = (
|
|
365
|
-
(config.method ||
|
|
369
|
+
(config.method || defaultMethod || "get") as string
|
|
366
370
|
).toLowerCase();
|
|
367
371
|
|
|
368
372
|
// Flatten headers
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// @ts-self-types="./FaxiosError.d.ts" — required for Deno: index.d.ts re-exports SchemaValidationError from here
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
|
+
import type { FaxiosResponse, InternalFaxiosRequestConfig } from "../types.js";
|
|
4
5
|
import type { StandardSchemaV1 } from "../types/standard-schema.js";
|
|
5
|
-
import type { InternalFaxiosRequestConfig, FaxiosResponse } from "../types.js";
|
|
6
6
|
import utils from "../utils.js";
|
|
7
7
|
import FaxiosHeaders from "./FaxiosHeaders.js";
|
|
8
8
|
|
|
@@ -247,9 +247,9 @@ export type SchemaValidationError =
|
|
|
247
247
|
}
|
|
248
248
|
| FaxiosError & {
|
|
249
249
|
code:
|
|
250
|
-
| typeof FaxiosError.ERR_BAD_REQUEST_SCHEMA
|
|
251
250
|
| typeof FaxiosError.ERR_BAD_PARAMS_SCHEMA
|
|
252
|
-
| typeof FaxiosError.ERR_BAD_PATH_PARAMS_SCHEMA
|
|
251
|
+
| typeof FaxiosError.ERR_BAD_PATH_PARAMS_SCHEMA
|
|
252
|
+
| typeof FaxiosError.ERR_BAD_REQUEST_SCHEMA;
|
|
253
253
|
issues: SchemaIssues;
|
|
254
254
|
};
|
|
255
255
|
|