graphql-http 1.22.4 → 1.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/lib/audits/render.js +2 -2
- package/lib/audits/render.mjs +2 -2
- package/lib/audits/server.js +23 -4
- package/lib/audits/server.mjs +23 -4
- package/lib/audits/utils.d.mts +1 -0
- package/lib/audits/utils.d.ts +1 -0
- package/lib/audits/utils.js +8 -0
- package/lib/audits/utils.mjs +8 -0
- package/lib/handler.d.mts +1 -1
- package/lib/handler.d.ts +1 -1
- package/lib/handler.js +1 -1
- package/lib/handler.mjs +1 -1
- package/lib/use/express.d.mts +32 -2
- package/lib/use/express.d.ts +32 -2
- package/lib/use/express.js +26 -2
- package/lib/use/express.mjs +26 -2
- package/package.json +4 -5
- package/umd/graphql-http-audits.js +33 -6
- package/umd/graphql-http-audits.min.js +1 -1
- package/umd/graphql-http-audits.min.js.gz +0 -0
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
<h6>Simple, pluggable, zero-dependency, <a href="https://graphql.github.io/graphql-over-http">GraphQL over HTTP spec</a> compliant server, client and audit suite.</h6>
|
|
7
7
|
|
|
8
|
-
[](https://github.com/graphql/graphql-http/actions?query=workflow%3A%22Continuous+integration%22) [](https://www.npmjs.com/package/graphql-http)
|
|
9
9
|
|
|
10
10
|
<i>Quickly check for compliance? Visit <b>[graphql-http.com](https://graphql-http.com)</b>!</i>
|
|
11
11
|
|
package/lib/audits/render.js
CHANGED
|
@@ -98,10 +98,10 @@ async function printAuditFail(result) {
|
|
|
98
98
|
const headers = {};
|
|
99
99
|
for (const [key, val] of res.headers.entries()) {
|
|
100
100
|
// some headers change on each run, dont report it
|
|
101
|
-
if (
|
|
101
|
+
if (['date', 'expires'].includes(key)) {
|
|
102
102
|
headers[key] = '<timestamp>';
|
|
103
103
|
}
|
|
104
|
-
else if (['cf-ray', 'server-timing', 'set-cookie'].includes(key)) {
|
|
104
|
+
else if (['cf-ray', 'server-timing', 'set-cookie', 'age'].includes(key)) {
|
|
105
105
|
headers[key] = '<omitted>';
|
|
106
106
|
}
|
|
107
107
|
else {
|
package/lib/audits/render.mjs
CHANGED
|
@@ -94,10 +94,10 @@ async function printAuditFail(result) {
|
|
|
94
94
|
const headers = {};
|
|
95
95
|
for (const [key, val] of res.headers.entries()) {
|
|
96
96
|
// some headers change on each run, dont report it
|
|
97
|
-
if (
|
|
97
|
+
if (['date', 'expires'].includes(key)) {
|
|
98
98
|
headers[key] = '<timestamp>';
|
|
99
99
|
}
|
|
100
|
-
else if (['cf-ray', 'server-timing', 'set-cookie'].includes(key)) {
|
|
100
|
+
else if (['cf-ray', 'server-timing', 'set-cookie', 'age'].includes(key)) {
|
|
101
101
|
headers[key] = '<omitted>';
|
|
102
102
|
}
|
|
103
103
|
else {
|
package/lib/audits/server.js
CHANGED
|
@@ -134,9 +134,10 @@ function serverAudits(opts) {
|
|
|
134
134
|
(0, utils_1.ressert)(res).status.toBeBetween(400, 499);
|
|
135
135
|
}),
|
|
136
136
|
// Request POST
|
|
137
|
-
(0, utils_1.audit)('9ABE', '
|
|
137
|
+
(0, utils_1.audit)('9ABE', 'SHOULD respond with 4xx status code if content-type is not supplied on POST requests', async () => {
|
|
138
138
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
139
139
|
method: 'POST',
|
|
140
|
+
body: JSON.stringify({ query: '{ __typename }' }),
|
|
140
141
|
});
|
|
141
142
|
(0, utils_1.ressert)(res).status.toBeBetween(400, 499);
|
|
142
143
|
}),
|
|
@@ -341,6 +342,7 @@ function serverAudits(opts) {
|
|
|
341
342
|
},
|
|
342
343
|
});
|
|
343
344
|
(0, utils_1.ressert)(res).status.toBe(200);
|
|
345
|
+
await (0, utils_1.ressert)(res).bodyAsExecutionResult.notToHaveProperty('errors');
|
|
344
346
|
}),
|
|
345
347
|
(0, utils_1.audit)('6A70', 'MAY allow URL-encoded JSON string {variables} parameter in GETs when accepting application/json', async () => {
|
|
346
348
|
const url = new URL(await getUrl(opts.url));
|
|
@@ -401,21 +403,38 @@ function serverAudits(opts) {
|
|
|
401
403
|
(0, utils_1.ressert)(res).status.toBe(200);
|
|
402
404
|
await (0, utils_1.ressert)(res).bodyAsExecutionResult.notToHaveProperty('errors');
|
|
403
405
|
}),
|
|
404
|
-
(0, utils_1.audit)('B6DC', 'MAY use 4xx or 5xx status codes on JSON parsing failure', async () => {
|
|
406
|
+
(0, utils_1.audit)('B6DC', 'MAY use 2xx, 4xx, or 5xx status codes on JSON parsing failure when accepting application/json', async () => {
|
|
405
407
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
406
408
|
method: 'POST',
|
|
407
409
|
headers: {
|
|
408
410
|
'content-type': 'application/json',
|
|
411
|
+
accept: 'application/json',
|
|
409
412
|
},
|
|
410
413
|
body: '{ "not a JSON',
|
|
411
414
|
});
|
|
412
|
-
(0, utils_1.ressert)(res).status.
|
|
415
|
+
(0, utils_1.ressert)(res).status.toBeBetweenMultiple([
|
|
416
|
+
[200, 299],
|
|
417
|
+
[400, 499],
|
|
418
|
+
[500, 599],
|
|
419
|
+
]);
|
|
413
420
|
}),
|
|
414
|
-
(0, utils_1.audit)('BCF8', '
|
|
421
|
+
(0, utils_1.audit)('BCF8', 'SHOULD use 400 status code on JSON parsing failure when accepting application/json', async () => {
|
|
415
422
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
416
423
|
method: 'POST',
|
|
417
424
|
headers: {
|
|
418
425
|
'content-type': 'application/json',
|
|
426
|
+
accept: 'application/json',
|
|
427
|
+
},
|
|
428
|
+
body: '{ "not a JSON',
|
|
429
|
+
});
|
|
430
|
+
(0, utils_1.ressert)(res).status.toBe(400);
|
|
431
|
+
}),
|
|
432
|
+
(0, utils_1.audit)('B7N8', 'SHOULD use 400 status code on JSON parsing failure when accepting application/graphql-response+json', async () => {
|
|
433
|
+
const res = await fetchFn(await getUrl(opts.url), {
|
|
434
|
+
method: 'POST',
|
|
435
|
+
headers: {
|
|
436
|
+
'content-type': 'application/json',
|
|
437
|
+
accept: 'application/graphql-response+json',
|
|
419
438
|
},
|
|
420
439
|
body: '{ "not a JSON',
|
|
421
440
|
});
|
package/lib/audits/server.mjs
CHANGED
|
@@ -131,9 +131,10 @@ export function serverAudits(opts) {
|
|
|
131
131
|
ressert(res).status.toBeBetween(400, 499);
|
|
132
132
|
}),
|
|
133
133
|
// Request POST
|
|
134
|
-
audit('9ABE', '
|
|
134
|
+
audit('9ABE', 'SHOULD respond with 4xx status code if content-type is not supplied on POST requests', async () => {
|
|
135
135
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
136
136
|
method: 'POST',
|
|
137
|
+
body: JSON.stringify({ query: '{ __typename }' }),
|
|
137
138
|
});
|
|
138
139
|
ressert(res).status.toBeBetween(400, 499);
|
|
139
140
|
}),
|
|
@@ -338,6 +339,7 @@ export function serverAudits(opts) {
|
|
|
338
339
|
},
|
|
339
340
|
});
|
|
340
341
|
ressert(res).status.toBe(200);
|
|
342
|
+
await ressert(res).bodyAsExecutionResult.notToHaveProperty('errors');
|
|
341
343
|
}),
|
|
342
344
|
audit('6A70', 'MAY allow URL-encoded JSON string {variables} parameter in GETs when accepting application/json', async () => {
|
|
343
345
|
const url = new URL(await getUrl(opts.url));
|
|
@@ -398,21 +400,38 @@ export function serverAudits(opts) {
|
|
|
398
400
|
ressert(res).status.toBe(200);
|
|
399
401
|
await ressert(res).bodyAsExecutionResult.notToHaveProperty('errors');
|
|
400
402
|
}),
|
|
401
|
-
audit('B6DC', 'MAY use 4xx or 5xx status codes on JSON parsing failure', async () => {
|
|
403
|
+
audit('B6DC', 'MAY use 2xx, 4xx, or 5xx status codes on JSON parsing failure when accepting application/json', async () => {
|
|
402
404
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
403
405
|
method: 'POST',
|
|
404
406
|
headers: {
|
|
405
407
|
'content-type': 'application/json',
|
|
408
|
+
accept: 'application/json',
|
|
406
409
|
},
|
|
407
410
|
body: '{ "not a JSON',
|
|
408
411
|
});
|
|
409
|
-
ressert(res).status.
|
|
412
|
+
ressert(res).status.toBeBetweenMultiple([
|
|
413
|
+
[200, 299],
|
|
414
|
+
[400, 499],
|
|
415
|
+
[500, 599],
|
|
416
|
+
]);
|
|
410
417
|
}),
|
|
411
|
-
audit('BCF8', '
|
|
418
|
+
audit('BCF8', 'SHOULD use 400 status code on JSON parsing failure when accepting application/json', async () => {
|
|
412
419
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
413
420
|
method: 'POST',
|
|
414
421
|
headers: {
|
|
415
422
|
'content-type': 'application/json',
|
|
423
|
+
accept: 'application/json',
|
|
424
|
+
},
|
|
425
|
+
body: '{ "not a JSON',
|
|
426
|
+
});
|
|
427
|
+
ressert(res).status.toBe(400);
|
|
428
|
+
}),
|
|
429
|
+
audit('B7N8', 'SHOULD use 400 status code on JSON parsing failure when accepting application/graphql-response+json', async () => {
|
|
430
|
+
const res = await fetchFn(await getUrl(opts.url), {
|
|
431
|
+
method: 'POST',
|
|
432
|
+
headers: {
|
|
433
|
+
'content-type': 'application/json',
|
|
434
|
+
accept: 'application/graphql-response+json',
|
|
416
435
|
},
|
|
417
436
|
body: '{ "not a JSON',
|
|
418
437
|
});
|
package/lib/audits/utils.d.mts
CHANGED
|
@@ -41,6 +41,7 @@ export declare function ressert(res: Response): {
|
|
|
41
41
|
status: {
|
|
42
42
|
toBe(code: number): void;
|
|
43
43
|
toBeBetween: (min: number, max: number) => void;
|
|
44
|
+
toBeBetweenMultiple: (ranges: Array<[number, number]>) => void;
|
|
44
45
|
};
|
|
45
46
|
header(key: 'content-type'): {
|
|
46
47
|
toContain(part: string): void;
|
package/lib/audits/utils.d.ts
CHANGED
|
@@ -41,6 +41,7 @@ export declare function ressert(res: Response): {
|
|
|
41
41
|
status: {
|
|
42
42
|
toBe(code: number): void;
|
|
43
43
|
toBeBetween: (min: number, max: number) => void;
|
|
44
|
+
toBeBetweenMultiple: (ranges: Array<[number, number]>) => void;
|
|
44
45
|
};
|
|
45
46
|
header(key: 'content-type'): {
|
|
46
47
|
toContain(part: string): void;
|
package/lib/audits/utils.js
CHANGED
|
@@ -97,6 +97,14 @@ function ressert(res) {
|
|
|
97
97
|
throw new AuditError(res, `Response status is not between ${min} and ${max}`);
|
|
98
98
|
}
|
|
99
99
|
},
|
|
100
|
+
toBeBetweenMultiple: (ranges) => {
|
|
101
|
+
const isInRange = ranges.some(([min, max]) => min <= res.status && res.status <= max);
|
|
102
|
+
if (!isInRange) {
|
|
103
|
+
throw new AuditError(res, `Response status is not between any of the provided ranges: ${ranges
|
|
104
|
+
.map(([min, max]) => `[${min}, ${max}]`)
|
|
105
|
+
.join(', ')}`);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
100
108
|
},
|
|
101
109
|
header(key) {
|
|
102
110
|
return {
|
package/lib/audits/utils.mjs
CHANGED
|
@@ -78,6 +78,14 @@ export function ressert(res) {
|
|
|
78
78
|
throw new AuditError(res, `Response status is not between ${min} and ${max}`);
|
|
79
79
|
}
|
|
80
80
|
},
|
|
81
|
+
toBeBetweenMultiple: (ranges) => {
|
|
82
|
+
const isInRange = ranges.some(([min, max]) => min <= res.status && res.status <= max);
|
|
83
|
+
if (!isInRange) {
|
|
84
|
+
throw new AuditError(res, `Response status is not between any of the provided ranges: ${ranges
|
|
85
|
+
.map(([min, max]) => `[${min}, ${max}]`)
|
|
86
|
+
.join(', ')}`);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
81
89
|
},
|
|
82
90
|
header(key) {
|
|
83
91
|
return {
|
package/lib/handler.d.mts
CHANGED
|
@@ -201,7 +201,7 @@ export interface HandlerOptions<RequestRaw = unknown, RequestContext = unknown,
|
|
|
201
201
|
getOperationAST?: typeof graphqlGetOperationAST;
|
|
202
202
|
/**
|
|
203
203
|
* The GraphQL root value or resolvers to go alongside the execution.
|
|
204
|
-
* Learn more about them here: https://graphql.org/learn/execution/#root-fields-resolvers.
|
|
204
|
+
* Learn more about them here: https://graphql.org/learn/execution/#root-fields-and-resolvers.
|
|
205
205
|
*
|
|
206
206
|
* If you return from `onSubscribe`, and the returned value is
|
|
207
207
|
* missing the `rootValue` field, the relevant operation root
|
package/lib/handler.d.ts
CHANGED
|
@@ -201,7 +201,7 @@ export interface HandlerOptions<RequestRaw = unknown, RequestContext = unknown,
|
|
|
201
201
|
getOperationAST?: typeof graphqlGetOperationAST;
|
|
202
202
|
/**
|
|
203
203
|
* The GraphQL root value or resolvers to go alongside the execution.
|
|
204
|
-
* Learn more about them here: https://graphql.org/learn/execution/#root-fields-resolvers.
|
|
204
|
+
* Learn more about them here: https://graphql.org/learn/execution/#root-fields-and-resolvers.
|
|
205
205
|
*
|
|
206
206
|
* If you return from `onSubscribe`, and the returned value is
|
|
207
207
|
* missing the `rootValue` field, the relevant operation root
|
package/lib/handler.js
CHANGED
|
@@ -308,7 +308,7 @@ function createHandler(options) {
|
|
|
308
308
|
return makeResponse(new graphql_1.GraphQLError('Subscriptions are not supported'), acceptedMediaType, formatError);
|
|
309
309
|
}
|
|
310
310
|
// mutations cannot happen over GETs
|
|
311
|
-
// https://graphql.github.io/graphql-over-http/draft/#sel-
|
|
311
|
+
// https://graphql.github.io/graphql-over-http/draft/#sel-GALJTPABABgCzsd
|
|
312
312
|
if (operation === 'mutation' && req.method === 'GET') {
|
|
313
313
|
return [
|
|
314
314
|
JSON.stringify({
|
package/lib/handler.mjs
CHANGED
|
@@ -304,7 +304,7 @@ export function createHandler(options) {
|
|
|
304
304
|
return makeResponse(new GraphQLError('Subscriptions are not supported'), acceptedMediaType, formatError);
|
|
305
305
|
}
|
|
306
306
|
// mutations cannot happen over GETs
|
|
307
|
-
// https://graphql.github.io/graphql-over-http/draft/#sel-
|
|
307
|
+
// https://graphql.github.io/graphql-over-http/draft/#sel-GALJTPABABgCzsd
|
|
308
308
|
if (operation === 'mutation' && req.method === 'GET') {
|
|
309
309
|
return [
|
|
310
310
|
JSON.stringify({
|
package/lib/use/express.d.mts
CHANGED
|
@@ -7,6 +7,12 @@ import { RequestParams } from '../common.mjs';
|
|
|
7
7
|
* @category Server/express
|
|
8
8
|
*/
|
|
9
9
|
export interface RequestContext {
|
|
10
|
+
/**
|
|
11
|
+
* The Express response object for the current request.
|
|
12
|
+
*
|
|
13
|
+
* This can be used to manipulate the HTTP response, for example to set
|
|
14
|
+
* response headers or cookies.
|
|
15
|
+
*/
|
|
10
16
|
res: Response;
|
|
11
17
|
}
|
|
12
18
|
/**
|
|
@@ -59,16 +65,40 @@ export type HandlerOptions<Context extends OperationContext = undefined> = RawHa
|
|
|
59
65
|
* Create a GraphQL over HTTP spec compliant request handler for
|
|
60
66
|
* the express framework.
|
|
61
67
|
*
|
|
68
|
+
* The Express response is available through `req.context.res`. To expose it
|
|
69
|
+
* to GraphQL resolvers, return it from the `context` option. This can be used
|
|
70
|
+
* to manipulate the HTTP response, for example to set response headers or
|
|
71
|
+
* cookies.
|
|
72
|
+
*
|
|
62
73
|
* ```js
|
|
63
74
|
* import express from 'express'; // yarn add express
|
|
64
75
|
* import { createHandler } from 'graphql-http/lib/use/express';
|
|
65
76
|
* import { schema } from './my-graphql-schema/index.mjs';
|
|
66
77
|
*
|
|
67
78
|
* const app = express();
|
|
68
|
-
*
|
|
79
|
+
*
|
|
80
|
+
* app.all(
|
|
81
|
+
* '/graphql',
|
|
82
|
+
* createHandler({
|
|
83
|
+
* schema,
|
|
84
|
+
* context(req) {
|
|
85
|
+
* return {
|
|
86
|
+
* res: req.context.res,
|
|
87
|
+
* };
|
|
88
|
+
* },
|
|
89
|
+
* }),
|
|
90
|
+
* );
|
|
69
91
|
*
|
|
70
92
|
* app.listen({ port: 4000 });
|
|
71
|
-
*
|
|
93
|
+
* ```
|
|
94
|
+
*
|
|
95
|
+
* A resolver can then manipulate the response:
|
|
96
|
+
*
|
|
97
|
+
* ```js
|
|
98
|
+
* resolve(_, __, { res }) {
|
|
99
|
+
* res.setHeader('set-cookie', 'session=; Max-Age=0; Path=/');
|
|
100
|
+
* return true;
|
|
101
|
+
* }
|
|
72
102
|
* ```
|
|
73
103
|
*
|
|
74
104
|
* @category Server/express
|
package/lib/use/express.d.ts
CHANGED
|
@@ -7,6 +7,12 @@ import { RequestParams } from '../common';
|
|
|
7
7
|
* @category Server/express
|
|
8
8
|
*/
|
|
9
9
|
export interface RequestContext {
|
|
10
|
+
/**
|
|
11
|
+
* The Express response object for the current request.
|
|
12
|
+
*
|
|
13
|
+
* This can be used to manipulate the HTTP response, for example to set
|
|
14
|
+
* response headers or cookies.
|
|
15
|
+
*/
|
|
10
16
|
res: Response;
|
|
11
17
|
}
|
|
12
18
|
/**
|
|
@@ -59,16 +65,40 @@ export type HandlerOptions<Context extends OperationContext = undefined> = RawHa
|
|
|
59
65
|
* Create a GraphQL over HTTP spec compliant request handler for
|
|
60
66
|
* the express framework.
|
|
61
67
|
*
|
|
68
|
+
* The Express response is available through `req.context.res`. To expose it
|
|
69
|
+
* to GraphQL resolvers, return it from the `context` option. This can be used
|
|
70
|
+
* to manipulate the HTTP response, for example to set response headers or
|
|
71
|
+
* cookies.
|
|
72
|
+
*
|
|
62
73
|
* ```js
|
|
63
74
|
* import express from 'express'; // yarn add express
|
|
64
75
|
* import { createHandler } from 'graphql-http/lib/use/express';
|
|
65
76
|
* import { schema } from './my-graphql-schema';
|
|
66
77
|
*
|
|
67
78
|
* const app = express();
|
|
68
|
-
*
|
|
79
|
+
*
|
|
80
|
+
* app.all(
|
|
81
|
+
* '/graphql',
|
|
82
|
+
* createHandler({
|
|
83
|
+
* schema,
|
|
84
|
+
* context(req) {
|
|
85
|
+
* return {
|
|
86
|
+
* res: req.context.res,
|
|
87
|
+
* };
|
|
88
|
+
* },
|
|
89
|
+
* }),
|
|
90
|
+
* );
|
|
69
91
|
*
|
|
70
92
|
* app.listen({ port: 4000 });
|
|
71
|
-
*
|
|
93
|
+
* ```
|
|
94
|
+
*
|
|
95
|
+
* A resolver can then manipulate the response:
|
|
96
|
+
*
|
|
97
|
+
* ```js
|
|
98
|
+
* resolve(_, __, { res }) {
|
|
99
|
+
* res.setHeader('set-cookie', 'session=; Max-Age=0; Path=/');
|
|
100
|
+
* return true;
|
|
101
|
+
* }
|
|
72
102
|
* ```
|
|
73
103
|
*
|
|
74
104
|
* @category Server/express
|
package/lib/use/express.js
CHANGED
|
@@ -56,16 +56,40 @@ exports.parseRequestParams = parseRequestParams;
|
|
|
56
56
|
* Create a GraphQL over HTTP spec compliant request handler for
|
|
57
57
|
* the express framework.
|
|
58
58
|
*
|
|
59
|
+
* The Express response is available through `req.context.res`. To expose it
|
|
60
|
+
* to GraphQL resolvers, return it from the `context` option. This can be used
|
|
61
|
+
* to manipulate the HTTP response, for example to set response headers or
|
|
62
|
+
* cookies.
|
|
63
|
+
*
|
|
59
64
|
* ```js
|
|
60
65
|
* import express from 'express'; // yarn add express
|
|
61
66
|
* import { createHandler } from 'graphql-http/lib/use/express';
|
|
62
67
|
* import { schema } from './my-graphql-schema';
|
|
63
68
|
*
|
|
64
69
|
* const app = express();
|
|
65
|
-
*
|
|
70
|
+
*
|
|
71
|
+
* app.all(
|
|
72
|
+
* '/graphql',
|
|
73
|
+
* createHandler({
|
|
74
|
+
* schema,
|
|
75
|
+
* context(req) {
|
|
76
|
+
* return {
|
|
77
|
+
* res: req.context.res,
|
|
78
|
+
* };
|
|
79
|
+
* },
|
|
80
|
+
* }),
|
|
81
|
+
* );
|
|
66
82
|
*
|
|
67
83
|
* app.listen({ port: 4000 });
|
|
68
|
-
*
|
|
84
|
+
* ```
|
|
85
|
+
*
|
|
86
|
+
* A resolver can then manipulate the response:
|
|
87
|
+
*
|
|
88
|
+
* ```js
|
|
89
|
+
* resolve(_, __, { res }) {
|
|
90
|
+
* res.setHeader('set-cookie', 'session=; Max-Age=0; Path=/');
|
|
91
|
+
* return true;
|
|
92
|
+
* }
|
|
69
93
|
* ```
|
|
70
94
|
*
|
|
71
95
|
* @category Server/express
|
package/lib/use/express.mjs
CHANGED
|
@@ -52,16 +52,40 @@ export async function parseRequestParams(req, res) {
|
|
|
52
52
|
* Create a GraphQL over HTTP spec compliant request handler for
|
|
53
53
|
* the express framework.
|
|
54
54
|
*
|
|
55
|
+
* The Express response is available through `req.context.res`. To expose it
|
|
56
|
+
* to GraphQL resolvers, return it from the `context` option. This can be used
|
|
57
|
+
* to manipulate the HTTP response, for example to set response headers or
|
|
58
|
+
* cookies.
|
|
59
|
+
*
|
|
55
60
|
* ```js
|
|
56
61
|
* import express from 'express'; // yarn add express
|
|
57
62
|
* import { createHandler } from 'graphql-http/lib/use/express';
|
|
58
63
|
* import { schema } from './my-graphql-schema/index.mjs';
|
|
59
64
|
*
|
|
60
65
|
* const app = express();
|
|
61
|
-
*
|
|
66
|
+
*
|
|
67
|
+
* app.all(
|
|
68
|
+
* '/graphql',
|
|
69
|
+
* createHandler({
|
|
70
|
+
* schema,
|
|
71
|
+
* context(req) {
|
|
72
|
+
* return {
|
|
73
|
+
* res: req.context.res,
|
|
74
|
+
* };
|
|
75
|
+
* },
|
|
76
|
+
* }),
|
|
77
|
+
* );
|
|
62
78
|
*
|
|
63
79
|
* app.listen({ port: 4000 });
|
|
64
|
-
*
|
|
80
|
+
* ```
|
|
81
|
+
*
|
|
82
|
+
* A resolver can then manipulate the response:
|
|
83
|
+
*
|
|
84
|
+
* ```js
|
|
85
|
+
* resolve(_, __, { res }) {
|
|
86
|
+
* res.setHeader('set-cookie', 'session=; Max-Age=0; Path=/');
|
|
87
|
+
* return true;
|
|
88
|
+
* }
|
|
65
89
|
* ```
|
|
66
90
|
*
|
|
67
91
|
* @category Server/express
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "graphql-http",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.1",
|
|
4
4
|
"description": "Simple, pluggable, zero-dependency, GraphQL over HTTP spec compliant server, client and audit suite.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"graphql",
|
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
"implementations/**/*"
|
|
114
114
|
],
|
|
115
115
|
"peerDependencies": {
|
|
116
|
-
"graphql": ">=0.11 <=
|
|
116
|
+
"graphql": ">=0.11 <=17"
|
|
117
117
|
},
|
|
118
118
|
"devDependencies": {
|
|
119
119
|
"@cspell/cspell-types": "^8.1.2",
|
|
@@ -126,7 +126,6 @@
|
|
|
126
126
|
"@types/express": "^4.17.21",
|
|
127
127
|
"@types/glob": "^8.1.0",
|
|
128
128
|
"@types/html-validator": "^5.0.6",
|
|
129
|
-
"@types/k6": "^0.47.3",
|
|
130
129
|
"@types/koa": "^2.15.0",
|
|
131
130
|
"@types/koa-mount": "^4.0.5",
|
|
132
131
|
"@typescript-eslint/eslint-plugin": "^6.13.2",
|
|
@@ -138,7 +137,7 @@
|
|
|
138
137
|
"express": "^4.18.2",
|
|
139
138
|
"fastify": "^4.24.3",
|
|
140
139
|
"glob": "^10.3.10",
|
|
141
|
-
"graphql": "^
|
|
140
|
+
"graphql": "^17.0.0",
|
|
142
141
|
"html-validator": "^6.0.1",
|
|
143
142
|
"koa": "^2.15.3",
|
|
144
143
|
"koa-mount": "^4.0.0",
|
|
@@ -152,7 +151,7 @@
|
|
|
152
151
|
"typedoc": "^0.25.4",
|
|
153
152
|
"typedoc-plugin-markdown": "^3.17.1",
|
|
154
153
|
"typescript": "^5.3.2",
|
|
155
|
-
"uWebSockets.js": "uNetworking/uWebSockets.js#v20.
|
|
154
|
+
"uWebSockets.js": "uNetworking/uWebSockets.js#v20.69.0",
|
|
156
155
|
"vitest": "^1.0.1"
|
|
157
156
|
}
|
|
158
157
|
}
|
|
@@ -99,6 +99,14 @@
|
|
|
99
99
|
throw new AuditError(res, `Response status is not between ${min} and ${max}`);
|
|
100
100
|
}
|
|
101
101
|
},
|
|
102
|
+
toBeBetweenMultiple: (ranges) => {
|
|
103
|
+
const isInRange = ranges.some(([min, max]) => min <= res.status && res.status <= max);
|
|
104
|
+
if (!isInRange) {
|
|
105
|
+
throw new AuditError(res, `Response status is not between any of the provided ranges: ${ranges
|
|
106
|
+
.map(([min, max]) => `[${min}, ${max}]`)
|
|
107
|
+
.join(', ')}`);
|
|
108
|
+
}
|
|
109
|
+
},
|
|
102
110
|
},
|
|
103
111
|
header(key) {
|
|
104
112
|
return {
|
|
@@ -296,9 +304,10 @@
|
|
|
296
304
|
ressert(res).status.toBeBetween(400, 499);
|
|
297
305
|
}),
|
|
298
306
|
// Request POST
|
|
299
|
-
audit('9ABE', '
|
|
307
|
+
audit('9ABE', 'SHOULD respond with 4xx status code if content-type is not supplied on POST requests', async () => {
|
|
300
308
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
301
309
|
method: 'POST',
|
|
310
|
+
body: JSON.stringify({ query: '{ __typename }' }),
|
|
302
311
|
});
|
|
303
312
|
ressert(res).status.toBeBetween(400, 499);
|
|
304
313
|
}),
|
|
@@ -503,6 +512,7 @@
|
|
|
503
512
|
},
|
|
504
513
|
});
|
|
505
514
|
ressert(res).status.toBe(200);
|
|
515
|
+
await ressert(res).bodyAsExecutionResult.notToHaveProperty('errors');
|
|
506
516
|
}),
|
|
507
517
|
audit('6A70', 'MAY allow URL-encoded JSON string {variables} parameter in GETs when accepting application/json', async () => {
|
|
508
518
|
const url = new URL(await getUrl(opts.url));
|
|
@@ -563,21 +573,38 @@
|
|
|
563
573
|
ressert(res).status.toBe(200);
|
|
564
574
|
await ressert(res).bodyAsExecutionResult.notToHaveProperty('errors');
|
|
565
575
|
}),
|
|
566
|
-
audit('B6DC', 'MAY use 4xx or 5xx status codes on JSON parsing failure', async () => {
|
|
576
|
+
audit('B6DC', 'MAY use 2xx, 4xx, or 5xx status codes on JSON parsing failure when accepting application/json', async () => {
|
|
567
577
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
568
578
|
method: 'POST',
|
|
569
579
|
headers: {
|
|
570
580
|
'content-type': 'application/json',
|
|
581
|
+
accept: 'application/json',
|
|
571
582
|
},
|
|
572
583
|
body: '{ "not a JSON',
|
|
573
584
|
});
|
|
574
|
-
ressert(res).status.
|
|
585
|
+
ressert(res).status.toBeBetweenMultiple([
|
|
586
|
+
[200, 299],
|
|
587
|
+
[400, 499],
|
|
588
|
+
[500, 599],
|
|
589
|
+
]);
|
|
575
590
|
}),
|
|
576
|
-
audit('BCF8', '
|
|
591
|
+
audit('BCF8', 'SHOULD use 400 status code on JSON parsing failure when accepting application/json', async () => {
|
|
577
592
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
578
593
|
method: 'POST',
|
|
579
594
|
headers: {
|
|
580
595
|
'content-type': 'application/json',
|
|
596
|
+
accept: 'application/json',
|
|
597
|
+
},
|
|
598
|
+
body: '{ "not a JSON',
|
|
599
|
+
});
|
|
600
|
+
ressert(res).status.toBe(400);
|
|
601
|
+
}),
|
|
602
|
+
audit('B7N8', 'SHOULD use 400 status code on JSON parsing failure when accepting application/graphql-response+json', async () => {
|
|
603
|
+
const res = await fetchFn(await getUrl(opts.url), {
|
|
604
|
+
method: 'POST',
|
|
605
|
+
headers: {
|
|
606
|
+
'content-type': 'application/json',
|
|
607
|
+
accept: 'application/graphql-response+json',
|
|
581
608
|
},
|
|
582
609
|
body: '{ "not a JSON',
|
|
583
610
|
});
|
|
@@ -867,10 +894,10 @@
|
|
|
867
894
|
const headers = {};
|
|
868
895
|
for (const [key, val] of res.headers.entries()) {
|
|
869
896
|
// some headers change on each run, dont report it
|
|
870
|
-
if (
|
|
897
|
+
if (['date', 'expires'].includes(key)) {
|
|
871
898
|
headers[key] = '<timestamp>';
|
|
872
899
|
}
|
|
873
|
-
else if (['cf-ray', 'server-timing', 'set-cookie'].includes(key)) {
|
|
900
|
+
else if (['cf-ray', 'server-timing', 'set-cookie', 'age'].includes(key)) {
|
|
874
901
|
headers[key] = '<omitted>';
|
|
875
902
|
}
|
|
876
903
|
else {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).graphqlHttpAudits={})}(this,(function(e){"use strict";function t(e){return null===e?"null":Array.isArray(e)?"array":typeof e}function a(e,t,a){return{id:e,name:t,fn:async()=>{try{return await a(),{id:e,name:t,status:"ok"}}catch(a){if(!(a instanceof n))throw a;return{id:e,name:t,status:t.startsWith("MUST")?"error":t.startsWith("SHOULD")?"warn":"notice",reason:a.reason,response:a.response}}}}}class n{constructor(e,t){this.response=e,this.reason=t}}function o(e){return{status:{toBe(t){if(e.status!==t)throw new n(e,`Response status code is not ${t}`)},toBeBetween:(t,a)=>{if(!(t<=e.status&&e.status<=a))throw new n(e,`Response status is not between ${t} and ${a}`)}},header:t=>({toContain(a){var o;if(!(null===(o=e.headers.get(t))||void 0===o?void 0:o.includes(a)))throw new n(e,`Response header ${t} does not contain ${a}`)},notToContain(a){var o;if(null===(o=e.headers.get(t))||void 0===o?void 0:o.includes(a))throw new n(e,`Response header ${t} contains ${a}`)}}),bodyAsExecutionResult:{data:{async toBe(t){const a=e.clone();if((await s(e)).data!==t)throw new n(a,`Response body execution result data is not "${t}"`)}},async toHaveProperty(t){const a=e.clone();if(!(t in await s(e)))throw new n(a,`Response body execution result does not have a property "${t}"`)},async notToHaveProperty(t){const a=e.clone();if(t in await s(e))throw new n(a,`Response body execution result has a property "${t}"`)}}}}async function s(e){let t,a;try{const a=new TextDecoder("utf-8"),n=await e.arrayBuffer();t=a.decode(n)}catch(t){throw new n(e,"Response body is not UTF-8 encoded")}try{a=JSON.parse(t)}catch(t){throw new n(e,"Response body is not valid JSON")}return a}function i(e){const s=e.fetchFn||fetch;return[a("22EB","SHOULD accept application/graphql-response+json and match the content-type",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200),o(t).header("content-type").toContain("application/graphql-response+json")})),a("4655","MUST accept application/json and match the content-type",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200),o(t).header("content-type").toContain("application/json")})),a("47DE","SHOULD accept */* and use application/json for the content-type",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"*/*"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200),o(t).header("content-type").toContain("application/json")})),a("80D8","SHOULD assume application/json content-type when accept is missing",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200),o(t).header("content-type").toContain("application/json")})),a("82A3","MUST use utf-8 encoding when responding",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200);try{new TextDecoder("utf-8").decode(await t.arrayBuffer())}catch(e){throw new n(t,"Response body is not UTF-8 encoded")}})),a("BF61","MUST accept utf-8 encoded request",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json; charset=utf-8"},body:JSON.stringify({query:'{ __type(name: "Run🏃Swim🏊") { name } }'})})).status.toBe(200)})),a("78D5","MUST assume utf-8 in request if encoding is unspecified",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }"})})).status.toBe(200)})),a("2C94","MUST accept POST requests",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }"})})).status.toBe(200)})),a("5A70","MAY accept application/x-www-form-urlencoded formatted GET requests",(async()=>{const t=new URL(await r(e.url));t.searchParams.set("query","{ __typename }");o(await s(t.toString())).status.toBe(200)})),a("9C48","MAY NOT allow executing mutations on GET requests",(async()=>{const t=new URL(await r(e.url));t.searchParams.set("query","mutation { __typename }");o(await s(t.toString(),{headers:{accept:"application/graphql-response+json"}})).status.toBeBetween(400,499)})),a("9ABE","MAY respond with 4xx status code if content-type is not supplied on POST requests",(async()=>{o(await s(await r(e.url),{method:"POST"})).status.toBeBetween(400,499)})),a("03D4","MUST accept application/json POST requests",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }"})})).status.toBe(200)})),a("A5BF","MAY use 400 status code when request body is missing on POST",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"}})).status.toBe(400)})),a("423L","MAY use 400 status code on missing {query} parameter",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({notquery:"{ __typename }"})})).status.toBe(400)})),...[{obj:"ect"},0,!1,["array"]].map(((n,i)=>a(`LKJ${i}`,`MAY use 400 status code on ${t(n)} {query} parameter`,(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:n})})).status.toBe(400)})))),a("34A2","SHOULD allow string {query} parameter when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ __typename }"})})).status.toBe(200)})),a("13EE","MUST allow string {query} parameter when accepting application/json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200),await o(t).bodyAsExecutionResult.notToHaveProperty("errors")})),...[{obj:"ect"},0,!1,["array"]].map(((n,i)=>a(`6C0${i}`,`MAY use 400 status code on ${t(n)} {operationName} parameter`,(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({operationName:n,query:"{ __typename }"})})).status.toBe(400)})))),a("8161","SHOULD allow string {operationName} parameter when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({operationName:"Query",query:"query Query { __typename }"})})).status.toBe(200)})),a("B8B3","MUST allow string {operationName} parameter when accepting application/json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({operationName:"Query",query:"query Query { __typename }"})});o(t).status.toBe(200),await o(t).bodyAsExecutionResult.notToHaveProperty("errors")})),...["variables","operationName","extensions"].flatMap(((t,n)=>[a(`94B${n}`,`SHOULD allow null {${t}} parameter when accepting application/graphql-response+json`,(async()=>{const a=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ __typename }",[t]:null})});o(a).status.toBe(200),await o(a).bodyAsExecutionResult.notToHaveProperty("errors")})),a(`022${n}`,`MUST allow null {${t}} parameter when accepting application/json`,(async()=>{const a=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ __typename }",[t]:null})});o(a).status.toBe(200),await o(a).bodyAsExecutionResult.notToHaveProperty("errors")}))])),...["string",0,!1,["array"]].map(((n,i)=>a(`476${i}`,`MAY use 400 status code on ${t(n)} {variables} parameter`,(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }",variables:n})})).status.toBe(400)})))),a("2EA1","SHOULD allow map {variables} parameter when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"query Type($name: String!) { __type(name: $name) { name } }",variables:{name:"sometype"}})})).status.toBe(200)})),a("28B9","MUST allow map {variables} parameter when accepting application/json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"query Type($name: String!) { __type(name: $name) { name } }",variables:{name:"sometype"}})});o(t).status.toBe(200),await o(t).bodyAsExecutionResult.notToHaveProperty("errors")})),a("D6D5","MAY allow URL-encoded JSON string {variables} parameter in GETs when accepting application/graphql-response+json",(async()=>{const t=new URL(await r(e.url));t.searchParams.set("query","query Type($name: String!) { __type(name: $name) { name } }"),t.searchParams.set("variables",JSON.stringify({name:"sometype"}));o(await s(t.toString(),{method:"GET",headers:{accept:"application/graphql-response+json"}})).status.toBe(200)})),a("6A70","MAY allow URL-encoded JSON string {variables} parameter in GETs when accepting application/json",(async()=>{const t=new URL(await r(e.url));t.searchParams.set("query","query Type($name: String!) { __type(name: $name) { name } }"),t.searchParams.set("variables",JSON.stringify({name:"sometype"}));const a=await s(t.toString(),{method:"GET",headers:{accept:"application/json"}});o(a).status.toBe(200),await o(a).bodyAsExecutionResult.notToHaveProperty("errors")})),...["string",0,!1,["array"]].map(((n,i)=>a(`58B${i}`,`MAY use 400 status code on ${t(n)} {extensions} parameter`,(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }",extensions:n})})).status.toBe(400)})))),a("428F","SHOULD allow map {extensions} parameter when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ __typename }",extensions:{some:"value"}})})).status.toBe(200)})),a("1B7A","MUST allow map {extensions} parameter when accepting application/json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ __typename }",extensions:{some:"value"}})});o(t).status.toBe(200),await o(t).bodyAsExecutionResult.notToHaveProperty("errors")})),a("B6DC","MAY use 4xx or 5xx status codes on JSON parsing failure",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:'{ "not a JSON'})).status.toBeBetween(400,499)})),a("BCF8","MAY use 400 status code on JSON parsing failure",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:'{ "not a JSON'})).status.toBe(400)})),a("8764","MAY use 4xx or 5xx status codes if parameters are invalid",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({qeury:"{ __typename }"})})).status.toBeBetween(400,599)})),a("3E3A","MAY use 400 status code if parameters are invalid",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({qeury:"{ __typename }"})})).status.toBe(400)})),a("572B","SHOULD use 200 status code on document parsing failure when accepting application/json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{"})})).status.toBe(200)})),a("FDE2","SHOULD use 200 status code on document validation failure when accepting application/json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ 8f31403dfe404bccbb0e835f2629c6a7 }"})})).status.toBe(200)})),a("7B9B","SHOULD use a status code of 200 on variable coercion failure when accepting application/json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"query CoerceFailure($id: ID!){ __typename }",variables:{id:null}})})).status.toBe(200)})),a("865D","SHOULD use 4xx or 5xx status codes on document parsing failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{"})})).status.toBeBetween(400,599)})),a("556A","SHOULD use 400 status code on document parsing failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{"})})).status.toBe(400)})),a("D586","SHOULD not contain the data entry on document parsing failure when accepting application/graphql-response+json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{"})});await o(t).bodyAsExecutionResult.data.toBe(void 0)})),a("51FE","SHOULD use 4xx or 5xx status codes on document validation failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ 8f31403dfe404bccbb0e835f2629c6a7 }"})})).status.toBeBetween(400,599)})),a("74FF","SHOULD use 400 status code on document validation failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ 8f31403dfe404bccbb0e835f2629c6a7 }"})})).status.toBe(400)})),a("5E5B","SHOULD not contain the data entry on document validation failure when accepting application/graphql-response+json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ 8f31403dfe404bccbb0e835f2629c6a7 }"})});await o(t).bodyAsExecutionResult.data.toBe(void 0)})),a("86EE","SHOULD use a status code of 400 on variable coercion failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"query CoerceFailure($id: ID!){ __typename }",variables:{id:null}})})).status.toBe(400)}))]}async function r(e){return"function"==typeof e?await e():e}async function p(e){var t;let a="";a+=`<li><code>${e.id}</code> ${e.name}\n`,a+="<details>\n",a+=`<summary>${function(e,t=1024){if(e.length>t)return e.substring(0,t)+"...";return e}(e.reason)}</summary>\n`,a+='<pre><code class="lang-json">';const n=e.response,o={};for(const[e,t]of n.headers.entries())"date"===e?o[e]="<timestamp>":["cf-ray","server-timing","set-cookie"].includes(e)?o[e]="<omitted>":o[e]=t;let s,i="";try{i=await n.text(),s=JSON.parse(i)}catch(e){(null===(t=n.headers.get("content-type"))||void 0===t?void 0:t.includes("text/html"))&&(i="<html omitted>")}return a+=JSON.stringify({status:n.status,statusText:n.statusText,headers:o,body:s||((null==i?void 0:i.length)>5120?"<body is too long>":i)||null},((e,t)=>{if(null!=t&&"object"==typeof t&&!Array.isArray(t)){const e={};return Object.keys(t).sort().reverse().reduce(((e,a)=>(e[a]=t[a],e)),e)}return t}),2)+"\n",a+="</code></pre>\n",a+="</details>\n",a+="</li>\n",a}e.auditServer=async function(e){const t=i(e);return await Promise.all(t.map((({fn:e})=>e())))},e.renderAuditResultsToHTML=async function(e){const t={total:0,ok:[],notice:[],warn:[],error:[]};for(const a of e)t.total++,a.status,t[a.status].push(a);let a="<i>* This report was auto-generated by graphql-http</i>\n";if(a+="\n",a+="<h1>GraphQL over HTTP audit report</h1>\n",a+="\n",a+="<ul>\n",a+=`<li><b>${t.total}</b> audits in total</li>\n`,t.ok.length&&(a+=`<li><span style="font-family: monospace">✅</span> <b>${t.ok.length}</b> pass</li>\n`),t.notice.length&&(a+=`<li><span style="font-family: monospace">💡</span> <b>${t.notice.length}</b> notices (suggestions)</li>\n`),t.warn.length&&(a+=`<li><span style="font-family: monospace">❗️</span> <b>${t.warn.length}</b> warnings (optional)</li>\n`),t.error.length&&(a+=`<li><span style="font-family: monospace">❌</span> <b>${t.error.length}</b> errors (required)</li>\n`),a+="</ul>\n",a+="\n",t.ok.length){a+="<h2>Passing</h2>\n",a+="<ol>\n";for(const[,e]of t.ok.entries())a+=`<li><code>${e.id}</code> ${e.name}</li>\n`;a+="</ol>\n",a+="\n"}if(t.notice.length){a+="<h2>Notices</h2>\n",a+="The server <i>MAY</i> support these, but are truly optional. These are suggestions following recommended conventions.\n",a+="<ol>\n";for(const[,e]of t.notice.entries())a+=await p(e);a+="</ol>\n",a+="\n"}if(t.warn.length){a+="<h2>Warnings</h2>\n",a+="The server <i>SHOULD</i> support these, but is not required.\n",a+="<ol>\n";for(const[,e]of t.warn.entries())a+=await p(e);a+="</ol>\n",a+="\n"}if(t.error.length){a+="<h2>Errors</h2>\n",a+="The server <b>MUST</b> support these.\n",a+="<ol>\n";for(const[,e]of t.error.entries())a+=await p(e);a+="</ol>\n"}return a},e.serverAudits=i}));
|
|
1
|
+
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).graphqlHttpAudits={})}(this,(function(e){"use strict";function t(e){return null===e?"null":Array.isArray(e)?"array":typeof e}function a(e,t,a){return{id:e,name:t,fn:async()=>{try{return await a(),{id:e,name:t,status:"ok"}}catch(a){if(!(a instanceof n))throw a;return{id:e,name:t,status:t.startsWith("MUST")?"error":t.startsWith("SHOULD")?"warn":"notice",reason:a.reason,response:a.response}}}}}class n{constructor(e,t){this.response=e,this.reason=t}}function o(e){return{status:{toBe(t){if(e.status!==t)throw new n(e,`Response status code is not ${t}`)},toBeBetween:(t,a)=>{if(!(t<=e.status&&e.status<=a))throw new n(e,`Response status is not between ${t} and ${a}`)},toBeBetweenMultiple:t=>{if(!t.some((([t,a])=>t<=e.status&&e.status<=a)))throw new n(e,`Response status is not between any of the provided ranges: ${t.map((([e,t])=>`[${e}, ${t}]`)).join(", ")}`)}},header:t=>({toContain(a){var o;if(!(null===(o=e.headers.get(t))||void 0===o?void 0:o.includes(a)))throw new n(e,`Response header ${t} does not contain ${a}`)},notToContain(a){var o;if(null===(o=e.headers.get(t))||void 0===o?void 0:o.includes(a))throw new n(e,`Response header ${t} contains ${a}`)}}),bodyAsExecutionResult:{data:{async toBe(t){const a=e.clone();if((await s(e)).data!==t)throw new n(a,`Response body execution result data is not "${t}"`)}},async toHaveProperty(t){const a=e.clone();if(!(t in await s(e)))throw new n(a,`Response body execution result does not have a property "${t}"`)},async notToHaveProperty(t){const a=e.clone();if(t in await s(e))throw new n(a,`Response body execution result has a property "${t}"`)}}}}async function s(e){let t,a;try{const a=new TextDecoder("utf-8"),n=await e.arrayBuffer();t=a.decode(n)}catch(t){throw new n(e,"Response body is not UTF-8 encoded")}try{a=JSON.parse(t)}catch(t){throw new n(e,"Response body is not valid JSON")}return a}function i(e){const s=e.fetchFn||fetch;return[a("22EB","SHOULD accept application/graphql-response+json and match the content-type",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200),o(t).header("content-type").toContain("application/graphql-response+json")})),a("4655","MUST accept application/json and match the content-type",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200),o(t).header("content-type").toContain("application/json")})),a("47DE","SHOULD accept */* and use application/json for the content-type",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"*/*"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200),o(t).header("content-type").toContain("application/json")})),a("80D8","SHOULD assume application/json content-type when accept is missing",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200),o(t).header("content-type").toContain("application/json")})),a("82A3","MUST use utf-8 encoding when responding",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200);try{new TextDecoder("utf-8").decode(await t.arrayBuffer())}catch(e){throw new n(t,"Response body is not UTF-8 encoded")}})),a("BF61","MUST accept utf-8 encoded request",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json; charset=utf-8"},body:JSON.stringify({query:'{ __type(name: "Run🏃Swim🏊") { name } }'})})).status.toBe(200)})),a("78D5","MUST assume utf-8 in request if encoding is unspecified",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }"})})).status.toBe(200)})),a("2C94","MUST accept POST requests",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }"})})).status.toBe(200)})),a("5A70","MAY accept application/x-www-form-urlencoded formatted GET requests",(async()=>{const t=new URL(await r(e.url));t.searchParams.set("query","{ __typename }");o(await s(t.toString())).status.toBe(200)})),a("9C48","MAY NOT allow executing mutations on GET requests",(async()=>{const t=new URL(await r(e.url));t.searchParams.set("query","mutation { __typename }");o(await s(t.toString(),{headers:{accept:"application/graphql-response+json"}})).status.toBeBetween(400,499)})),a("9ABE","SHOULD respond with 4xx status code if content-type is not supplied on POST requests",(async()=>{o(await s(await r(e.url),{method:"POST",body:JSON.stringify({query:"{ __typename }"})})).status.toBeBetween(400,499)})),a("03D4","MUST accept application/json POST requests",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }"})})).status.toBe(200)})),a("A5BF","MAY use 400 status code when request body is missing on POST",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"}})).status.toBe(400)})),a("423L","MAY use 400 status code on missing {query} parameter",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({notquery:"{ __typename }"})})).status.toBe(400)})),...[{obj:"ect"},0,!1,["array"]].map(((n,i)=>a(`LKJ${i}`,`MAY use 400 status code on ${t(n)} {query} parameter`,(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:n})})).status.toBe(400)})))),a("34A2","SHOULD allow string {query} parameter when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ __typename }"})})).status.toBe(200)})),a("13EE","MUST allow string {query} parameter when accepting application/json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200),await o(t).bodyAsExecutionResult.notToHaveProperty("errors")})),...[{obj:"ect"},0,!1,["array"]].map(((n,i)=>a(`6C0${i}`,`MAY use 400 status code on ${t(n)} {operationName} parameter`,(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({operationName:n,query:"{ __typename }"})})).status.toBe(400)})))),a("8161","SHOULD allow string {operationName} parameter when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({operationName:"Query",query:"query Query { __typename }"})})).status.toBe(200)})),a("B8B3","MUST allow string {operationName} parameter when accepting application/json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({operationName:"Query",query:"query Query { __typename }"})});o(t).status.toBe(200),await o(t).bodyAsExecutionResult.notToHaveProperty("errors")})),...["variables","operationName","extensions"].flatMap(((t,n)=>[a(`94B${n}`,`SHOULD allow null {${t}} parameter when accepting application/graphql-response+json`,(async()=>{const a=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ __typename }",[t]:null})});o(a).status.toBe(200),await o(a).bodyAsExecutionResult.notToHaveProperty("errors")})),a(`022${n}`,`MUST allow null {${t}} parameter when accepting application/json`,(async()=>{const a=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ __typename }",[t]:null})});o(a).status.toBe(200),await o(a).bodyAsExecutionResult.notToHaveProperty("errors")}))])),...["string",0,!1,["array"]].map(((n,i)=>a(`476${i}`,`MAY use 400 status code on ${t(n)} {variables} parameter`,(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }",variables:n})})).status.toBe(400)})))),a("2EA1","SHOULD allow map {variables} parameter when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"query Type($name: String!) { __type(name: $name) { name } }",variables:{name:"sometype"}})})).status.toBe(200)})),a("28B9","MUST allow map {variables} parameter when accepting application/json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"query Type($name: String!) { __type(name: $name) { name } }",variables:{name:"sometype"}})});o(t).status.toBe(200),await o(t).bodyAsExecutionResult.notToHaveProperty("errors")})),a("D6D5","MAY allow URL-encoded JSON string {variables} parameter in GETs when accepting application/graphql-response+json",(async()=>{const t=new URL(await r(e.url));t.searchParams.set("query","query Type($name: String!) { __type(name: $name) { name } }"),t.searchParams.set("variables",JSON.stringify({name:"sometype"}));const a=await s(t.toString(),{method:"GET",headers:{accept:"application/graphql-response+json"}});o(a).status.toBe(200),await o(a).bodyAsExecutionResult.notToHaveProperty("errors")})),a("6A70","MAY allow URL-encoded JSON string {variables} parameter in GETs when accepting application/json",(async()=>{const t=new URL(await r(e.url));t.searchParams.set("query","query Type($name: String!) { __type(name: $name) { name } }"),t.searchParams.set("variables",JSON.stringify({name:"sometype"}));const a=await s(t.toString(),{method:"GET",headers:{accept:"application/json"}});o(a).status.toBe(200),await o(a).bodyAsExecutionResult.notToHaveProperty("errors")})),...["string",0,!1,["array"]].map(((n,i)=>a(`58B${i}`,`MAY use 400 status code on ${t(n)} {extensions} parameter`,(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }",extensions:n})})).status.toBe(400)})))),a("428F","SHOULD allow map {extensions} parameter when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ __typename }",extensions:{some:"value"}})})).status.toBe(200)})),a("1B7A","MUST allow map {extensions} parameter when accepting application/json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ __typename }",extensions:{some:"value"}})});o(t).status.toBe(200),await o(t).bodyAsExecutionResult.notToHaveProperty("errors")})),a("B6DC","MAY use 2xx, 4xx, or 5xx status codes on JSON parsing failure when accepting application/json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:'{ "not a JSON'})).status.toBeBetweenMultiple([[200,299],[400,499],[500,599]])})),a("BCF8","SHOULD use 400 status code on JSON parsing failure when accepting application/json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:'{ "not a JSON'})).status.toBe(400)})),a("B7N8","SHOULD use 400 status code on JSON parsing failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:'{ "not a JSON'})).status.toBe(400)})),a("8764","MAY use 4xx or 5xx status codes if parameters are invalid",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({qeury:"{ __typename }"})})).status.toBeBetween(400,599)})),a("3E3A","MAY use 400 status code if parameters are invalid",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({qeury:"{ __typename }"})})).status.toBe(400)})),a("572B","SHOULD use 200 status code on document parsing failure when accepting application/json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{"})})).status.toBe(200)})),a("FDE2","SHOULD use 200 status code on document validation failure when accepting application/json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ 8f31403dfe404bccbb0e835f2629c6a7 }"})})).status.toBe(200)})),a("7B9B","SHOULD use a status code of 200 on variable coercion failure when accepting application/json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"query CoerceFailure($id: ID!){ __typename }",variables:{id:null}})})).status.toBe(200)})),a("865D","SHOULD use 4xx or 5xx status codes on document parsing failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{"})})).status.toBeBetween(400,599)})),a("556A","SHOULD use 400 status code on document parsing failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{"})})).status.toBe(400)})),a("D586","SHOULD not contain the data entry on document parsing failure when accepting application/graphql-response+json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{"})});await o(t).bodyAsExecutionResult.data.toBe(void 0)})),a("51FE","SHOULD use 4xx or 5xx status codes on document validation failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ 8f31403dfe404bccbb0e835f2629c6a7 }"})})).status.toBeBetween(400,599)})),a("74FF","SHOULD use 400 status code on document validation failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ 8f31403dfe404bccbb0e835f2629c6a7 }"})})).status.toBe(400)})),a("5E5B","SHOULD not contain the data entry on document validation failure when accepting application/graphql-response+json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ 8f31403dfe404bccbb0e835f2629c6a7 }"})});await o(t).bodyAsExecutionResult.data.toBe(void 0)})),a("86EE","SHOULD use a status code of 400 on variable coercion failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"query CoerceFailure($id: ID!){ __typename }",variables:{id:null}})})).status.toBe(400)}))]}async function r(e){return"function"==typeof e?await e():e}async function p(e){var t;let a="";a+=`<li><code>${e.id}</code> ${e.name}\n`,a+="<details>\n",a+=`<summary>${function(e,t=1024){if(e.length>t)return e.substring(0,t)+"...";return e}(e.reason)}</summary>\n`,a+='<pre><code class="lang-json">';const n=e.response,o={};for(const[e,t]of n.headers.entries())["date","expires"].includes(e)?o[e]="<timestamp>":["cf-ray","server-timing","set-cookie","age"].includes(e)?o[e]="<omitted>":o[e]=t;let s,i="";try{i=await n.text(),s=JSON.parse(i)}catch(e){(null===(t=n.headers.get("content-type"))||void 0===t?void 0:t.includes("text/html"))&&(i="<html omitted>")}return a+=JSON.stringify({status:n.status,statusText:n.statusText,headers:o,body:s||((null==i?void 0:i.length)>5120?"<body is too long>":i)||null},((e,t)=>{if(null!=t&&"object"==typeof t&&!Array.isArray(t)){const e={};return Object.keys(t).sort().reverse().reduce(((e,a)=>(e[a]=t[a],e)),e)}return t}),2)+"\n",a+="</code></pre>\n",a+="</details>\n",a+="</li>\n",a}e.auditServer=async function(e){const t=i(e);return await Promise.all(t.map((({fn:e})=>e())))},e.renderAuditResultsToHTML=async function(e){const t={total:0,ok:[],notice:[],warn:[],error:[]};for(const a of e)t.total++,a.status,t[a.status].push(a);let a="<i>* This report was auto-generated by graphql-http</i>\n";if(a+="\n",a+="<h1>GraphQL over HTTP audit report</h1>\n",a+="\n",a+="<ul>\n",a+=`<li><b>${t.total}</b> audits in total</li>\n`,t.ok.length&&(a+=`<li><span style="font-family: monospace">✅</span> <b>${t.ok.length}</b> pass</li>\n`),t.notice.length&&(a+=`<li><span style="font-family: monospace">💡</span> <b>${t.notice.length}</b> notices (suggestions)</li>\n`),t.warn.length&&(a+=`<li><span style="font-family: monospace">❗️</span> <b>${t.warn.length}</b> warnings (optional)</li>\n`),t.error.length&&(a+=`<li><span style="font-family: monospace">❌</span> <b>${t.error.length}</b> errors (required)</li>\n`),a+="</ul>\n",a+="\n",t.ok.length){a+="<h2>Passing</h2>\n",a+="<ol>\n";for(const[,e]of t.ok.entries())a+=`<li><code>${e.id}</code> ${e.name}</li>\n`;a+="</ol>\n",a+="\n"}if(t.notice.length){a+="<h2>Notices</h2>\n",a+="The server <i>MAY</i> support these, but are truly optional. These are suggestions following recommended conventions.\n",a+="<ol>\n";for(const[,e]of t.notice.entries())a+=await p(e);a+="</ol>\n",a+="\n"}if(t.warn.length){a+="<h2>Warnings</h2>\n",a+="The server <i>SHOULD</i> support these, but is not required.\n",a+="<ol>\n";for(const[,e]of t.warn.entries())a+=await p(e);a+="</ol>\n",a+="\n"}if(t.error.length){a+="<h2>Errors</h2>\n",a+="The server <b>MUST</b> support these.\n",a+="<ol>\n";for(const[,e]of t.error.entries())a+=await p(e);a+="</ol>\n"}return a},e.serverAudits=i}));
|
|
Binary file
|